☰
SQL注入绕WAF:深入理解Tamper脚本与数据库语法适配
2026/10/19 7:52:46 网站建设 项目流程

做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就是“企业大客车”,语法习惯差得很明显。我整理了一个对照表,方便你快速记忆:

功能项MySQLMSSQL
行注释符--、#--
块注释符/* *//* */
字符串拼接CONCAT(a,b)或||a + b
限制返回行数LIMIT 0,1SELECT 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的拦截逻辑,对齐注入点的输出方式。每次适配之前,花五分钟确认这三件事,比堆一晚上脚本效率高得多。你如果也在这个方向上踩过坑,可以按自己的数据库场景去改出专属脚本库。把常用套路沉淀下来,下次再遇上类似项目,十分钟就能跑通全流程。这个积累过程,才是这类工具真正值钱的地方。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询