做Web安全评估这几年,我经常在群里看到有人贴一段Tamper脚本问“为什么我跑了半天没反应”。每次点开看,payload本身没什么问题,缺的就是对目标数据库的那层适配。SQL注入的payload从来不是“一套走天下”,它跟数据库引擎的语法强绑定。同样一个注入点,丢在MySQL上可以稳定延时5秒,换到MSSQL上却像石沉大海,连个报错都不给你。这不一定是WAF太强,更可能是你的payload压根不在对方语法体系里。
Tamper脚本就是用来干这个的。它把通用payload“翻译”成目标数据库能读懂的写法:空格换成注释、关键字换成同义表达、字符串用编码替代、时间函数换成目标引擎专有的延迟写法。MySQL、MSSQL、Oracle、PostgreSQL,各自有各自的脾气,不按它们的规矩来,脚本再花哨也白搭。
这篇文章我从数据库差异说起,拆开Tamper脚本的执行过程,然后分别拿MySQL和MSSQL做完整场景演示:怎么选脚本、怎么定顺序、怎么自己写一个适配脚本。内容基本来自我实际调试中的经验和踩坑记录,属于网上文档不太容易翻到的那部分。适合刚开始接触Web安全测试、对sqlmap只停留在默认参数的人,也适合已经跑过几个项目、但每次遇到数据库切换就头疼的测试人员。所有操作请限定在你自己有授权的靶场、CTF或委托测试环境中。
1. 为什么说“同一句话”在不同数据库里含义完全不同
1.1 从注释符差异开始说起
SQL注入里,注释符是使用频率最高的“工具”。它的作用很简单,把查询语句后面我们不想要的部分“吃掉”。但不同数据库对注释符的支持程度完全不一样,这个细节往往决定了payload能不能落地。
MySQL支持三种注释风格:--(注意后面要有空格)、#、/* */。MSSQL只支持--和/* */,不支持#。如果你在某条payload里用了#去注释,丢到MSSQL上就会多出一个无法解析的字符,整个注入直接失效。这还只是第一层差异。
更隐蔽的坑是--这个注释符。它虽然两种数据库都支持,但都要求后面至少跟一个空白字符。你用--x或--#去截断语句,MySQL上可能还能过,MSSQL上却会当成普通文本继续解析,直接语法报错。sqlmap默认生成的payload尾部通常带--和-(比如-- -),这个写法就是兼容性考虑,能覆盖绝大多数数据库。但如果你手动精简payload,或者某个Tamper脚本把尾部的空格吃掉了,问题马上就会出现。我见过不止一次,脚本辛辛苦苦绕过了WAF,结果因为尾部注释缺少空格,目标数据库直接报错,功亏一篑。
1.2 功能关键字的分道扬镳
注释符只是表层差异。真正让payload无法通用的,是各个数据库在“限制返回行数”“字符串拼接”“时间延迟”这些核心功能上的语法分裂。
先拿限制返回行数来说。MySQL用LIMIT,MSSQL用TOP,Oracle用ROWNUM,PostgreSQL用LIMIT。sqlmap里很多用来逐行取数据的payload,在MySQL上是LIMIT 0,1,在MSSQL上就得换TOP 1。你要是拿MySQL的payload去测MSSQL,对方连语法都解析不过去,测试结果自然是一堆误报。
字符串拼接也是一样。MSSQL用+,MySQL用CONCAT(),Oracle和PostgreSQL用||。当注入点需要靠拼接字符串来构造可见输出时,拼接符选错就等于数据出不来。更麻烦的是,拼接符还影响WAF绕过:比如+在URL里会被解析成空格,测试时如果没做URL编码,payload传到服务端已经变了模样,你怎么调试都找不到问题在哪。
时间延迟函数是第三个典型差异。MySQL有SLEEP(n),MSSQL只有WAITFOR DELAY '0:0:n',PostgreSQL是pg_sleep(n)。sqlmap在检测基于时间的盲注时,会按目标数据库生成对应的延时语句,前提是它正确识别了后端引擎。如果你手动指定错了--dbms,或者直接用--technique=BT但没让sqlmap确认引擎,它生成的延时payload很可能就是目标引擎不认识的写法。结果看起来像是“不存在的注入点”,其实只是“不兼容的payload”。
1.3 适用场景:为什么Tamper是这一层需要的
讲到这里,有人会问:sqlmap本身就有数据库指纹识别,默认模式下它能自动切换payload,我为什么还要折腾Tamper脚本?答案是:默认模式解决的是“数据库语法差异”,而Tamper脚本解决的是“受到WAF或输入过滤限制时的绕行路线”。
举个例子。目标站点有一个查询参数,拦截了空格字符,你发送包含空格的payload,请求直接返回403。这种情况下,sqlmap默认的payload库再丰富也没用,因为几乎所有payload都带空格。Tamper脚本可以在payload发出去之前,把空格替换成/*注释*/或者%09制表符,让WAF的正则规则失效。它承担的是一个“传输前改写层”的角色。
所以,Tamper的高价值场景具备两个特征:第一,目标系统有明显输入过滤;第二,你希望在一个通用payload的基础上,快速适配目标数据库的具体约束。两者缺一,Tamper的作用就发挥不出来。这个理解一定要放在前面,不然很容易把Tamper当成万能开关。
2. 拆解Tamper:它到底在什么时候做了什么
2.1 一条payload的完整旅行线路
在sqlmap里,Tamper脚本不是挂在某个环节上的独立工具,而是深嵌在检测流程里的。一条注入payload从生成到发出,大致会经历这样一趟旅行:sqlmap根据目标数据库指纹和注入方式,从内置payload库中选出候选payload;真正发送之前,它按顺序调用你在--tamper参数里指定的所有Tamper脚本,对payload做字符串变换;然后,把变换后的结果拼到完整请求里,发给目标服务器;最后,根据响应内容判断这次注入是否命中。
理解这条链路特别重要。Tamper脚本的输入是“已经生成完毕的payload文本”,输出是“改写后的payload文本”,它不负责识别注入点,也不负责判断结果。它的工作范围仅限于字符串级的替换、插入、编码。很多人把Tamper理解成“自动化绕过一切”,这个预期本身就是错的。你如果连注入点都没确认好,脚本作用再大也无从谈起。
这里还要特别注意“顺序敏感”的问题。多个Tamper脚本叠加使用时,它们按你写的顺序依次执行。前一个脚本的输出就是后一个脚本的输入。如果你把“把空格替换成注释”和“把注释替换成另一种注释”两个脚本排在一起,第二个脚本可能把你原本已经构造好的注释又改掉了,直接摧毁payload。顺序错了,好脚本组合也会翻车。
2.2 常用脚本的“替换逻辑”逐个看
sqlmap自带的Tamper脚本有几十个,日常用到的其实就是那一批。我把几个高频脚本的核心逻辑列出来,方便你对照理解。
| 脚本名 | 替换逻辑 | 适用引擎 | 注意事项 |
|---|---|---|---|
| space2comment | 空格转/**/ | MySQL/MSSQL/Oracle/PG通用 | 别让注释符粘连破坏尾部收尾 |
| between | =转BETWEEN 1 AND 1,>转NOT BETWEEN 0 AND | 通用 | 语句变长,流量特征变大 |
| randomcase | 随机大小写混合 | 视排序规则而定 | 可能引入大小写敏感错误 |
| versionedmorekeywords | 用/*!50000 ... */包裹关键字 | 仅MySQL | 其他引擎会将其当普通注释 |
| apostrophemask | 将单引号替换为宽字节编码 | 特定环境 | 依赖服务端字符集配置 |
| charunicodeencode | 将字符转为Unicode编码 | 多数引擎 | 不适合对编码敏感的WAF |
space2comment是最基础的脚本,把所有空格字符替换为/**/。它的好处是几乎不改变payload的语义,因为主流数据库都支持块注释,注释内部的内容会被忽略。缺点是如果WAF连注释符号都拦,它就失效了。
between脚本把>替换成NOT BETWEEN 0 AND,把=替换成BETWEEN 1 AND 1。比如AND 1=1会变成AND 1 BETWEEN 1 AND 1,这种改写能让基于“关键字+符号组合”的WAF规则失效。它用到的BETWEEN在MySQL、MSSQL、Oracle里都支持,通用性尚可。
randomcase脚本随机把关键字改成大小写混合,比如AND变成AnD、SELECT变成sElEcT。原理是绕过对关键字大小写敏感的正则匹配。但副作用也明显:有些数据库对特定关键字的大小写敏感,或者WAF本身做了大小写归一化,这脚本不但没用,还可能引入语法错误。
versionedmorekeywords是MySQL特色脚本,利用MySQL的版本化注释/*!50000 ... */包裹关键字。MySQL会执行版本化注释里的内容,而多数WAF不认识这种结构。这个脚本只适用于MySQL,丢到MSSQL上就是负担,因为MSSQL会把整个注释内容当普通注释忽略掉。
2.3 先记下这几个选择顺序原则
第一,低风险脚本优先。像space2comment这种只动空格的,语义风险最低,放在前面。像randomcase这种动关键字的,语义风险高,放后面,或者干脆不用。第二,先兼容后绕过。首要目标不是最大化“绕过的变态程度”,而是保证payload可执行。任何让目标数据库产生语法错误的改写都是负分。第三,把“检测阶段”和“注入阶段”分开考虑。如果目标只有WAF在拦,检测阶段可以用轻量绕过,注入阶段再上完整绕过。sqlmap允许你在--tamper里写多组脚本,但这一步大多数人都没用到。
这三个原则看着简单,实际组合脚本时特别容易踩坑。很多人一上来就把randomcase和space2comment一起挂上,跑出大量超时,然后又不知道为什么,其实就是因为随机大小写破坏了某些库的敏感语法。
3. MySQL场景适配实录:从报错到正常出数据
3.1 这个场景的目标与限制
我拿一个自己做过的模拟项目说事,脱敏后流程完全一样。目标是一个小型电商后台的商品查询接口,URL形态类似http://目标站点/product.php?id=88。测试环境是一台Linux上的MySQL 5.7,中间挂了一层简单WAF规则:拦截包含空格的请求,拦截information_schema这个关键字,还拦SELECT的大小写归一形式。
一开始我用sqlmap默认参数直接跑:
sqlmap -u "http://目标站点/product.php?id=88" --batch结果跑了几分钟全是超时和失败,显示unable to retrieve。我第一反应是注入点可能不存在,但手工在浏览器里构造?id=88'会弹出一个数据库语法错误页面,这证明注入点存在。问题出在payload被WAF拦截,或者关键字触碰了规则。
3.2 配置步骤:选脚本、定顺序、跑起来
碰到这种情况,我一般不会一上来就堆一堆Tamper脚本,而是先确认目标确实是MySQL。通过报错页面里的数据库错误特征,能看到“near ... at line 1”这类典型MySQL报错,同时表名反引号风格也指向MySQL。确认之后选脚本就有的放矢了。
第一步,处理空格问题。目标拦空格,我选space2comment,把所有空格换成/**/。第二步,处理敏感词SELECT和information_schema。versionedmorekeywords可以把这些关键字用MySQL版本化注释包起来,但它会改动payload里所有符合特征的关键字,范围较广,容易误伤。我先不启用,改用between去替换条件里的等号和大于号,减少WAF对条件语句的识别。第三步,保留space2comment和between的组合,执行命令:
sqlmap -u "http://目标站点/product.php?id=88" \ --dbms mysql \ --tamper "space2comment,between" \ --random-agent \ --level 3 --risk 2 \ --batch这里的--dbms mysql是强制指定引擎,让sqlmap直接生成MySQL语法族的payload,减少对指纹识别的依赖。--level 3 --risk 2是扩大测试边界,覆盖更多注入位置和危险payload。跑起来之后,检测阶段很快给出“参数id存在基于布尔的盲注”提示。这说明Tamper组合至少把payload送到了服务端,而且没有被语法层面打断。
3.3 实测现象与二次调整
第一次跑通后,我发现WAF只拦了空格的明文和information_schema,没拦注释符,所以注入能成功。但代价是会话延迟变高,流量里明显多了很多/* */。为了效率,我把between去掉,因为目标的过滤规则里没有针对等号和大于号的拦截,去掉之后payload更简洁,检测更快。
这里有个值得说的坑:space2comment替换的是所有空格,包括sqlmap payload尾部用来断句的那个空格。某些payload形态下,尾部空格被替换成/**/之后,--注释符失效,整个查询会拼接出畸形语法。我在这个模拟项目上就遇到过,把space2comment和versionedmorekeywords一起用时,--和/**/粘在一起,导致MySQL直接报错。解决办法是把versionedmorekeywords去掉,或者单独为它排一个靠后的顺序,保证它只处理关键字不碰注释符。这种“次生故障”是Tamper组合里最常见的翻车点。
经过这两轮调整,最终在MySQL场景里用一个space2comment就完成了注入数据提取,全程几分钟,数据完整性也正常。这也给了我一个经验:Tamper脚本不是越多越好,能让WAF失效的最简组合才是最优组合。
4. MSSQL场景适配实录:换个司机还得换个车道
4.1 MSSQL与MySQL高频差异速查
如果说MySQL是“开源小跑车”,那MSSQL就是“企业大客车”,语法习惯差得很明显。我整理了一个对照表,方便你快速记忆:
| 功能项 | MySQL | MSSQL |
|---|---|---|
| 行注释符 | --、# | -- |
| 块注释符 | /* */ | /* */ |
| 字符串拼接 | CONCAT(a,b)或|| | a + b |
| 限制返回行数 | LIMIT 0,1 | SELECT TOP 1 ... |
| 延时语句 | SLEEP(5) | WAITFOR DELAY '0:0:5' |
| 版本查看 | VERSION() | @@VERSION |
| 标识符括起 | 反引号` | 方括号[] |
| 报错信息 | “near ... at line 1” | “Incorrect syntax near ...” |
这张表解决的就是“为什么同一个Tamper脚本在MSSQL上不好使”的根因。因为Tamper脚本干的是字符串变换,它并不知道自己改出来的文本最终会对着哪个引擎执行。如果你的脚本里织入了MySQL习惯的表达,MSSQL自然不认。
4.2 有效应对MSSQL的脚本组合
再拿另一个模拟项目举例。一个内部工单系统,后端是MSSQL,某个查询点能用单引号触发报错。目标WAF拦了两类东西:包含WAITFOR的文本、不区分大小写的SELECT写法(它会先做大小写归一化再匹配)。
这种情况不能直接依赖sqlmap自动生成的基于时间的payload,因为默认写法WAITFOR DELAY会被WAF正则命中。我的处理思路是:第一,禁用基于时间的检测,改用布尔盲注,从根上避开WAITFOR关键字;第二,对SELECT做大小写混合改写,破坏WAF的归一化匹配;第三,确保所有字符串拼接用的是MSSQL的+而不是CONCAT。
执行命令我写成这样:
sqlmap -u "http://目标站点/workorder.aspx?id=101" \ --dbms mssql \ --technique=B \ --tamper "space2comment,randomcase" \ --batch注意--technique=B只启用了布尔盲注,没有启用时间盲注。用这种方法,payload里完全绕开了WAITFOR,自然也不会踩WAF的规则。
randomcase在这里的作用就是让SELECT变成SeLeCt之类的形态,绕过大小写归一化正则。它有随机性,每次生成的形态不同,跑大量测试时可能出现偶发失败。我建议在做定点测试时,加一个自己写的只处理SELECT的脚本,可控性更高。这个操作我在下一部分演示。
4.3 一个针对“字符串拼接+延时判定”的小脚本
MSSQL场景里还有一个常见需求:某些注入点必须输出拼接后的结果,才能继续判断。默认payload里如果出现单引号字符串拼接,比如'abc'||'def',在MSSQL上肯定报错。手动写一个把||替换成+的脚本就很有用。
脚本逻辑很简单,读取原始payload,找到所有||,替换成+:
def tamper(payload, **kwargs): if payload: payload = payload.replace("||", "+") return payload就这么几行,却在我那个MSSQL模拟测试里解决了“页面永远只显示第一行数据”的问题。原因是那个注入点本身处于一个拼接字段中,如果不用+连接,其他列的数据根本进不了输出。
这类小脚本的价值不在于复杂度,而在于精准。针对一个具体瓶颈写二十行不到的代码,远远比堆十个通用脚本更有效率。
5. 从零写一个可用的自定义Tamper脚本
5.1 脚本骨架与关键字段解析
Tamper脚本本质就是一个Python模块,sqlmap会通过固定的导入路径加载它。一个最小可用的脚本包含三部分:一个说明优先级的变量__priority__、一个空实现的dependencies()函数,以及核心的tamper()函数。sqlmap调用时会把当前payload文本传进来,返回值就是改写后的payload。
#!/usr/bin/env python """ 自定义Tamper脚本示例 """ from lib.core.enums import PRIORITY __priority__ = PRIORITY.NORMAL def dependencies(): pass def tamper(payload, **kwargs): if payload: payload = payload.replace(" ", "/*t*/") return payload优先级变量决定了脚本在sqlmap内部排队时的位置,常见枚举值有LOWEST、LOW、NORMAL、HIGH、HIGHEST。普通自定义脚本用NORMAL就够了,除非你明确知道脚本里包含低风险或高风险变换。
5.2 三个可直接抄作业的自定义脚本
第一个是把“AND、OR”逻辑写成符号形式。在MySQL里可以把AND替换成&&、把OR替换成||,用于绕过只拦关键字的WAF:
def tamper(payload, **kwargs): if payload: payload = payload.replace("AND", "&&").replace("OR", "||") return payload但要注意,这个脚本只适合MySQL,MSSQL不把&&当逻辑与,会直接报错。所以不要试图在一个脚本里兼容所有数据库,宁可多写几个,再按目标引擎选择。
第二个是把URL编码的空格替换成制表符。有些WAF拦空格可能拦得不够严,只匹配了%20,那%09(制表符)、%0A(换行)也许就是漏网之鱼。脚本可以只做一层简单替换:
def tamper(payload, **kwargs): if payload: payload = payload.replace("%20", "%09") return payload第三个是“转义单引号的兜底”。某些情况下payload里的单引号会被WAF正则直接命中,可利用数据库的CHAR()函数把字符串拆成字符编码再拼接。但一个完整的语法解析版本代码量不小,实操中我很少写全功能版本,更多是针对某一列固定字段单独写死映射。比如目标查询点是某个固定ID段,就把对应字符串硬编码替换,先保证功能正确,再迁就通用性。
5.3 验证脚本效果的正确姿势
写完脚本不能直接上目标,先在本地做回归测试更稳妥。sqlmap本身提供了一套Tamper脚本测试框架,但你也可以自己搭一个轻量注入靶场,在网关层模拟WAF规则。这样迭代脚本时不至于把真实目标的流量和状态搞乱。
两个必备检查点:第一,脚本改写后的payload必须能通过目标数据库的语法解析。你检查改写结果里的字符串、注释符、括号数量是否配对,基本就能判断。第二,脚本必须“顺序可控”。如果一个脚本连续跑两次会得到不同结果,说明随机性太强或副作用太大,要慎重。
6. 踩坑记录:这些问题我调试了大半个晚上
6.1 常见问题速查表
| 问题现象 | 常见原因 | 处理思路 |
|---|---|---|
| 跑了很久全是超时 | Tamper把时间延迟关键字影响了 | 检查--technique,确认是否启用了时间盲注 |
| 有报错但取不到数据 | 尾部注释符被脚本吃掉 | 检查space2comment之后的--是否存在 |
SELECT被替换后报错 | 数据库对大小写敏感 | 减少randomcase使用,改用定点替换 |
| 脚本运行成功但结果为空 | 字符串拼接符不对 | 检查字段输出是否依赖+或CONCAT语法 |
| payload到服务器变成乱码 | 没有正确做URL编码 | 关注+号、空格在URL传输层的转换 |
这张表是我这几年的高频问题浓缩,后面每一项我都亲手踩过。
6.2 定位是“脚本问题”还是“注入点问题”的方法
如果脚本跑不出来,别急着改代码。我通常这样排查:先关掉所有Tamper,用最小payload手工验证注入点是否还存在。如果连手工的'、"都能让目标报错,说明注入点本身没问题,问题出在脚本。如果手工都没反应,先解决注入点,再谈Tamper。
确认是脚本问题后,分段排查:先用单个脚本跑,确认它能生效;再加第二个脚本,看组合是否破坏语义。我见过很多人一上来就挂五六个脚本,结果哪个脚本改坏了payload都分不清。老老实实从单个开始,排查成本会低很多。
另外,sqlmap有-v 6(最大程度详细输出)和-t(记录原始流量日志)这两个参数,在诊断Tamper问题时几乎是必备的。打开详细输出后,你能看到payload被发送前的完整形态,一眼就能判断Tamper是否按预期工作。这两个参数配合单独跑单脚本的方法,基本能定位90%以上的“Tamper神秘失灵”问题。
最后分享一个我的个人体会。Tamper脚本用得越久,我越觉得它不是用来“炫技”的,而是用来“对齐”的:对齐目标数据库的语法,对齐WAF的拦截逻辑,对齐注入点的输出方式。每次适配之前,花五分钟确认这三件事,比堆一晚上脚本效率高得多。你如果也在这个方向上踩过坑,可以按自己的数据库场景去改出专属脚本库。把常用套路沉淀下来,下次再遇上类似项目,十分钟就能跑通全流程。这个积累过程,才是这类工具真正值钱的地方。