首页 / 资讯中心 / 文章详情

SQL注入绕WAF:深入理解Tamper脚本与数据库语法适配

SQL注入绕WAF:深入理解Tamper脚本与数据库语法适配 ★ FEATURED ARTICLE
做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用LIMITMSSQL用TOPOracle用ROWNUMPostgreSQL用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:nPostgreSQL是pg_sleep(n)。sqlmap在检测基于时间的盲注时会按目标数据库生成对应的延时语句前提是它正确识别了后端引擎。如果你手动指定错了--dbms或者直接用--techniqueBT但没让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编码多数引擎不适合对编码敏感的WAFspace2comment是最基础的脚本把所有空格字符替换为/**/。它的好处是几乎不改变payload的语义因为主流数据库都支持块注释注释内部的内容会被忽略。缺点是如果WAF连注释符号都拦它就失效了。between脚本把替换成NOT BETWEEN 0 AND把替换成BETWEEN 1 AND 1。比如AND 11会变成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?id88。测试环境是一台Linux上的MySQL 5.7中间挂了一层简单WAF规则拦截包含空格的请求拦截information_schema这个关键字还拦SELECT的大小写归一形式。一开始我用sqlmap默认参数直接跑sqlmap -u http://目标站点/product.php?id88 --batch结果跑了几分钟全是超时和失败显示unable to retrieve。我第一反应是注入点可能不存在但手工在浏览器里构造?id88会弹出一个数据库语法错误页面这证明注入点存在。问题出在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?id88 \ --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?id101 \ --dbms mssql \ --techniqueB \ --tamper space2comment,randomcase \ --batch注意--techniqueB只启用了布尔盲注没有启用时间盲注。用这种方法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替换成||用于绕过只拦关键字的WAFdef tamper(payload, **kwargs): if payload: payload payload.replace(AND, ).replace(OR, ||) return payload但要注意这个脚本只适合MySQLMSSQL不把当逻辑与会直接报错。所以不要试图在一个脚本里兼容所有数据库宁可多写几个再按目标引擎选择。第二个是把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的拦截逻辑对齐注入点的输出方式。每次适配之前花五分钟确认这三件事比堆一晚上脚本效率高得多。你如果也在这个方向上踩过坑可以按自己的数据库场景去改出专属脚本库。把常用套路沉淀下来下次再遇上类似项目十分钟就能跑通全流程。这个积累过程才是这类工具真正值钱的地方。
阅读完成 · 觉得有帮助?
咨询建站