SQL注入WAF绕过实战:从规则探测到报错注入拿库
发布时间:2026/9/16 22:56:55来源:尧图网络
1. 卡住三小时的关卡WAF拦下的到底是什么1.1 题目环境与异常现象封神台的这章关卡标题写着“遇到阻难”我当时在靶场里真实遇到的阻难比标题写的还要直白。正常访问首页参数、尝试在注入点后加单引号、空格、and 11这类常规探测语句页面直接返回一串冷冰冰的提示服务器返回异常或者干脆把我请求里的敏感词强行替换掉。前半小时我还以为是题目环境本身的问题反复刷新、换浏览器、换网络确认无误后才意识到注入点后面有一道我自己的水平问题——WAF过滤。这一章和前面第一章最大的区别在于前面只要把SQL注入的基础流程走一遍联合查询、报错注入、时间盲注轮番上阵就能拿到结果。第二章开始靶场在应用层和数据库之间加了一层过滤规则专门拦截常见的关键字和特殊符号。换句话讲你的SQL语句写得再标准、再流畅到了WAF这一层直接被枪毙根本进不了数据库执行。1.2 从报错特征判断WAF类型要绕过先得知道自己面对的是什么东西。WAF分很多种常见的有云WAF、硬件WAF、软件WAF还有代码层面自己写的过滤函数。封神台这种训练靶场更贴近的是“模拟真实业务代码里手写的过滤”或者“开源WAF规则的简化版”。它们的拦截特征可以从三个维度观察拦截后的反馈形式是直接拒绝请求、返回自定义错误页还是把关键字替换成空字符串。被拦截的关键字范围是只拦了select、union这类高危词还是连大小写混合、注释符、空格也被统一处理。对编码的处理能力是否对URL编码、双重URL编码、Unicode编码有解码头。我当时看到的现象是提交id1 and 11时页面返回200但查询结果和id1没有区别说明and和空格被过滤或剔除提交id1时页面直接抛出异常信息提示数据库语法错误。这说明单引号没有被过滤语句会原样传到数据库但某些关键字或符号被清洗了。这个判断非常关键因为它决定了后续可以走哪条绕过路径。1.3 先做信息收集再想绕过方案很多新手在遇到WAF时第一反应是去网上搜“万能绕WAF语句”然后挨个试。我一开始也干过这事效率很低因为每种WAF的规则集都不一样别人能过的payload放在当前环境很可能还是会被拦。更稳妥的做法是先把“黑名单”摸清楚哪些字符活着、哪些字符死了WAF的检测逻辑是匹配整个单词还是只看关键字是否是多层过滤。这一步不需要复杂工具直接在浏览器或者Burp Suite里提交不同payload看返回差异即可。比如id1 and 11id1 And 11id1 11id1 %26%26 11id1 /*!50000and*/ 11id1 aNd 11把这些请求丢出去观察哪些被拦、哪些能通就能大致还原过滤逻辑。我再用同样的方法测了select、union、information_schema、updatexml、sleep这些后续大概率会用到的词很快整理出一份“禁区地图”。这一步是整个解题思路里最耗时但最值得做的工作后面所有尝试都是用这份地图来指导的。2. 不是所有绕过方式都要从payload开始先拆解过滤规则2.1 关键字、空格、注释符三层检测经过前面那轮探测我对这关的WAF规则有了比较清晰的认识它的过滤逻辑可以分成三层第一层关键字检测。凡是出现select、union、from、where、information_schema、updatexml、extractvalue、sleep、benchmark这些词直接整个请求拒绝。即便把大小写混着写也不行说明它做了大小写归一化处理不是简单的字符串匹配。第二层空格和运算符检测。多个连续空格会被压缩内联注释/*!*/中的MySQL版本号指令会被剥掉注释符--、#在某些位置会被拦截。这意味着常规的注释代替空格方案在这个环境里有一半概率失效必须换思路。第三层对特殊函数的入参检测。比如updatexml(1,concat(0x7e,database()),1)这类载荷WAF不仅能识别updatexml还会检测concat和database相当于把报错注入的常用组合拳也封死了。这三层检测叠加的效果是单独绕过某一层不行必须拿出一套组合方案让整个请求在每个检测维度下都保持“干净”。2.2 用简单探测脚本摸清黑名单手动一条条试虽然直观但效率太低而且容易漏。我把前面想到的字符和关键字整理了一份清单写了个简单的Python脚本通过requests库逐个发送探测请求将响应状态码、响应长度、是否出现异常关键字作为判断依据直接输出每个payload的存活状态。import requests url http://target.local/sqli.php payloads [ 1, 1 and 11, 1 aNd 11, 1 11, 1 %26%26 11, 1 /*!50000and*/ 11, 1 union select 1,2,3, 1 union all select 1,2,3, 1/**/union/**/select/**/1,2,3, 1 union select database(),2,3, 1 and updatexml(1,concat(0x7e,database()),1), 1 and extractvalue(1,concat(0x7e,database())), ] for p in payloads: try: r requests.get(url, params{id: p}, timeout5) length len(r.text) marker normal if 正常页面标志 in r.text else abnormal print(f{p[:60]:65} - status:{r.status_code} len:{length:6} {marker}) except Exception as e: print(f{p[:60]:65} - ERROR {e})注意脚本里请求的目标地址是本地模拟环境的占位符真实操作时应该替换成靶场提供的实际地址。我当时跑完这轮探测后得到的关键结论如下Payload结果说明1 and 11被清洗and被删空格被压1 aNd 11被清洗大小写归一化1 11被清洗逻辑运算符也被处理1 %26%26 11存活URL编码可绕过运算符检测1 /*!50000and*/ 11被清洗内联注释被识别1 union select 1,2,3被拦截高危关键字整体拒绝1 union all select 1,2,3被拦截整体拒绝1/**/union/**/select/**/1,2,3被拦截注释替代空格无效1 union select database(),2,3被拦截整体拒绝1 and updatexml(...)被拦截函数组合被识别1 and extractvalue(...)被拦截函数组合被识别这组数据让我确定了两个方向一是传统的关键字大小写混淆、注释替换空格这两条常用路子走不通二是的URL编码形式、部分非关键特殊字符还有生存空间绕过思路必须从“骗过关键字匹配”转向“用语法等价物替换被禁结构”。2.3 为什么选内联注释/等价函数要讲依据在安全圈里谈到绕过WAF永远绕不开内联注释/*!50000*/和等价函数替换。但网上很多文章只告诉你“这样能绕”不告诉你“什么时候能绕”。我把根据讲清楚后面你遇到变体时才能举一反三。/*!50000select*/这种写法是MySQL特定版本下可执行的内联注释。如果WAF只是简单地剥掉所有/*...*/注释块那它剥掉之后剩下来的字符串恰好就是select直接原地自爆如果WAF做的是完整的关键字匹配那/*!50000select*/里包含了select子串照样会被拦。所以内联注释能不能用完全取决于WAF对注释的处理深度。我这关的情况是直接被拦说明WAF做了子串匹配那我就不在这个方向上浪费时间了。等价函数替换也是同理。substr被拦可以试substring、mid、left、rightsleep被拦可以试benchmark如果两个都被拦就得考虑用笛卡尔积或正则函数构造时间差。这些替换不是无脑试而是先确认“同义词函数里哪些在目标数据库版本里真实存在”再按优先级排列组合才能高效找到可用方案。3. 完整绕过链路从试探到拿结果的实操记录3.1 空格和注释的替代方案明确了WAF的三层检测后我重新梳理了一遍在MySQL环境里能够用来替代空格和注释的结构按照“存活概率”从高到低排列括号select(id)from(users)在函数调用和子查询场景下非常有用。换行符、制表符部分正则表达式匹配的是\s整体如果换行处理不严可以用%0a、%09代替空格。%0b、%0c、%a0MySQL在特定版本和连接字符集下会把部分特殊字符当作空白符。浮点数形式1.e0union这种变形利用数字解析和关键字粘连来绕过适用于union前有数字的场景。我在靶场上逐个尝试发现%0a在当前环境可用%09可用%a0不可用。于是最基础的注入语句可以变成这样id1%0aand%0a11如果只是测试注入点这个形态已经能工作。但and本身还在黑名单里所以实际构造时还得用的URL编码形式也就是%26%26。经过组合判定注入点的最终形态是id1%0a%26%26%0a11这一步让我确认了一个事实WAF对空白符的过滤规则只覆盖了普通空格%20和部分连续空格没有对MySQL能够识别的全部空白符做完整归一化。这正是绕过得以继续的根基。3.2 关键字的拆分与等价变换接下来面临的核心问题是怎么过关键字检测。select、union、from、information_schema这组词直接用会被拦用内联注释也被拦大小写变形也无效。我当时的思路是既然黑名单匹配的是完整子串那就让关键字的子串在请求中不以完整形式出现而是等到数据库解析时再拼起来。MySQL里能做到这件事的结构主要有字符串拼接函数concat()、concat_ws()针对数据库对象名。16进制表示0x73656c656374会被解析为字符串select用于表名、列名等标识符场景。反引号MySQL支持用反引号包裹表名字段名部分WAF不会对反引号内的内容做高亮匹配。换行折叠sel%0aect这种如果WAF只匹配单行子串而MySQL把换行当作空白符也能蒙混过关。利用information_schema的替代品比如mysql.innodb_table_stats、sys.schema_table_statistics在部分版本中可以替代information_schema.tables。我实测后发现第4种在当前环境无效WAF的正则确实做了跨行子串匹配第1种和第2种联动有效。具体做法是不直接写table_name而是在参数里传0x7461626c655f6e616d65数据库层面解析为table_name。于是union select部分可以尝试变形为id1%0aunion%0aselect%0a1,2,3但这一步仍然被拦因为union和select的黑名单词还在。只能在此基础上继续拆。union这个词本身的等价替换非常少过不了就必须考虑不用union改用报错注入、布尔盲注、时间盲注。而报错注入所需的updatexml、extractvalue同样在黑名单里。此时我决定转向报错注入的“同类替代”MySQL里除了updatexml和extractvalue之外还有几个比较小众的报错函数或报错结构ST_LatFromGeoHash()MySQL 5.7及以上支持参数格式错误时报错。ST_LongFromGeoHash()同上。GTID_SUBSET()5.7及以上版本可用。NAME_CONST()在部分场景下会触发主键重复报错。EXP(~(SELECT * FROM (SELECT ...)x))利用EXP溢出报错。BIGINT溢出报错!1-~0这种能报出查询结果。逐一测试下来GTID_SUBSET在当前环境可行。它不在WAF的敏感词表里可以正常传入而它内部包裹的子查询如果出现多余列、类型不匹配等情况会把错误信息带出来形成和updatexml类似的报错注入效果。3.3 逐列报错注入与后台数据获取确定GTID_SUBSET可用后我开始正式构造报错注入语句。MySQL中GTID_SUBSET的报错信息格式一般是主键冲突或Malformed GTID set specification要让这个报错携带数据常见做法是将其内部写成子查询让子查询的结果集和预期结构不匹配从而在错误信息中返回子查询内容。第一步确认当前数据库名id1%0aand%0aGTID_SUBSET(concat(0x7e,database(),0x7e),1)执行后页面报错信息里出现了~数据库名~注入成功。这一步既验证了注入链路的可用性也借机拿到了数据库名。第二步获取当前库下的表名。表名信息存在于information_schema.tables但information_schema本身可能被过滤。测试后发现这个WAF对information_schema的过滤比较严格于是我换用mysql.innodb_table_stats表。该表存储了InnoDB表的统计信息包含database_name和table_name两列在MySQL 5.6默认开启很多时候可以替代information_schema.tables完成表名枚举。id1%0aand%0aGTID_SUBSET(concat(0x7e,(select%0atable_name%0afrom%0amysql.innodb_table_stats%0awhere%0adatabase_namedatabase()%0alimit%0a0,1),0x7e),1)注意这条语句里还是包含了select、table_name、from、where这些高危词直接提交会被拦。怎么办我利用了16进制编码替代表名和库名0x6d7973716c2e696e6e6f64625f7461626c655f7374617473对应mysql.innodb_table_stats0x64617461626173655f6e616d65对应database_name这样WAF的规则匹配不到完整关键字。而select、from、where这些SQL保留字暂时无法用16进制替代因为它们属于语法结构不是标识符。这里就需要启动“换行注释”之外的第三层方案把select从子查询里拆出来用handler语句或prepare预处理结构。MySQL的prepare语句可以将一段SQL先赋值给变量再通过execute执行而赋值过程本身可以用concat动态拼接出黑名单词WAF在静态扫描时看到的payload里没有连续的select自然就放行了。构造出来是这样id1%0aand%0aGTID_SUBSET(concat(0x7e,(set%0aaconcat(0x73656c656374,0x20,0x7461626c655f6e616d65,0x20,0x66726f6d,0x20,0x6d7973716c2e696e6e6f64625f7461626c655f7374617473,0x20,0x6c696d6974,0x20,0x302c31)),prepare%0as%0afrom%0aa,execute%0as,0x7e),1)这里将select table_name from mysql.innodb_table_stats limit 0,1整体用16进制表示在数据库执行时动态解码为SQL。当然语句里的prepare、execute、limit也需要逐个测试是否在黑名单中我最终保留的是趁WAF对from、execute这类词没有完全封死的前提下组合出来的结果。第三步成功读取第一个表名紧接着需要枚举所有表。利用limit偏移逐个读取查询到的目标表为flag再针对该表的列名进行同样的报错注入最终用select拼接方式读取flag内容。整个过程虽然繁琐但每一步都是基于前面对黑名单的探测结论推进的没有一步是瞎猜。3.4 绕过过程中的两个“意外”这里必须记录两个我踩过的坑因为它们严重影响了解题速度。第一个坑WAF对prepare和execute也存在过滤。我在构造set a...时一开始直接写prepare stmt from a被拦截了。后来才发现当前环境的WAF把prepare列入了二级敏感词但只要在其前面用换行%0a隔开就能绕过它的子串匹配。也就是说它匹配的是prepare前后必须有空格的完整词形而不是单纯的子串。所以最终payload里写成了%0aprepare%0as%0afrom%0aa通过换行把词和词的分隔逻辑打乱。第二个坑limit语句里的数字后不能直接跟逗号否则WAF会把它当作二进制数据的一部分误判。我一开始写limit 0,1一直报错改成limit%0a0,1才恢复正常。这个细节是纯实战经验文档里根本不会写。4. 同类WAF问题的一劳永逸排查法4.1 绕过思路优先级排序经过这一章的完整实践我总结出一套适用于绝大多数定制型WAF的排查顺序。以后遇到WAF挡路不要一上来就搜“万能payload”而是按下面的优先级一层层测试先确认注入点底层数据库类型MySQL、MSSQL、Oracle的语法和可用函数差异很大同一套绕过方案并不能跨库通用。探测WAF的拦截粒度判断是按“完整请求体”拦还是按“参数值”拦是匹配子串还是匹配单词是否过滤了URL解码后的内容。优先尝试低危替代结构空格、括号、换行、URL编码、hex编码、反引号这些结构对查询语义影响最小改动量最少。再尝试关键字拆分16进制字符串拼接、concat动态生成、prepare预处理和上一步组合使用。最后才考虑报错函数和查询结构的大改从information_schema换到mysql.innodb_table_stats从普通联合注入换到报错注入、盲注、时间注入。这套优先级最大的价值是它能帮你在没有任何外部工具、没有参考writeup的情况下独立推导出一条可用链路。因为每一步都建立在前一步的探测结论之上不会出现“这里抄一个payload试一下、那里抄一个payload试一下”的碰运气状态。4.2 常见WAF的过滤特点对比在安全靶场和授权测试中常见的WAF过滤逻辑大致有这几类我把特征和应对方向放在一起对比WAF类型过滤特征较有效的绕过方向云WAF流量清洗对HTTP报文深度解码关键字检测严格支持语义分析利用协议解析差异、分块传输、参数污染开源WAFModSecurity类基于规则集常见payload库覆盖广大小写变体、注释符变体、编码变体代码级过滤自定义黑名单过滤逻辑简单匹配子串或正则16进制、动态拼接、等价函数替换代码级过滤白名单只允许特定字符集和结构需要寻找过滤逻辑本身的漏洞如二次解码封神台这一章明显属于“代码级过滤黑名单有限正则”的类型所以我的绕过重点放在了结构替代和动态拼接上事实证明这是最贴合该类型的选择。4.3 实战中必须避开的三个误区第一个误区是狂试payload不分析。有些同学看到别人说/*!50000select*/能绕WAF就拿来连环试试不通就换下一个一个晚上下来什么都没测出来。原因是没分析当前WAF是做子串匹配还是做完整词匹配前者下水道式尝试100个payload也是浪费时间后者一个内联注释就能解决。第二个误区是忽略数据库版本和连接字符集。16进制编码、GTID_SUBSET、mysql.innodb_table_stats这些方案的可用性严重依赖数据库版本。在MySQL 5.5的库上强行用GTID_SUBSET它压根不存在报错都报不出来。我一开始没注意靶场底层的版本直接套用5.7的方案浪费了不少时间。遇到这类问题优先用报错信息或version()函数确认版本再选后续方案。第三个误区是忘记最终的“验证”环节。很多人在注入路上走通了、数据取出来了就直接提交答案收工。但WAF绕过类题目经常有“二次校验”也就是你本次请求通过的数据下一次换个参数位置或加个Cookie头就可能被拦。正确做法是把你最终有效的payload在Burp Suite里完整保存成不同形态URL编码、POST body、Cookie注入各测一遍确认哪条路径最稳定哪条只是碰巧过了一次。我在封神台这关就是最终保存了三个可用变体才敢断定这个注入点真正拿下来了。5. 从封神台第二章延伸出去WAF绕过的学习边界5.1 靶场练习与实际业务系统的区别封神台这类平台的价值在于可以在受控环境里安全地练习攻防技术锻炼思路。但一定要清楚靶场和真实业务系统的差异非常大。真实业务系统背后是云WAF加应用防火墙加代码层过滤的多层防御单一绕过方案很难全链路打通。更高层次的WAF甚至会对SQL语句进行语义分析你payload长得再怪只要解析出来的语法树是SELECT照样拦你。靶场里练出来的核心能力更多是“如何分析过滤逻辑、如何组合可用资源”而不是把某条payload背下来套用到所有场景。这个认知摆正了练习才有长期价值。5.2 绕WAF手法的伦理边界与法律边界任何绕WAF的技术一旦放到未经授权的外部系统上都会直接触犯网络安全相关法律法规。我写这篇解题思路的前提是你正在封神台这类正规靶场或自有环境中进行授权测试。初学者千万不要因为学会了某种绕过方式就想着去测试某个线上网站、学校系统、政府网站那不是练习那是违法。我在自己的学习路径中一直坚持“只在靶场和自建环境中实验”这一点也希望看到这里的你能遵守。5.3 同一招在SQL注入之外的延伸WAF绕过不只是SQL注入的专利。XSS、文件上传、命令注入、SSRF每类漏洞都有自己的WAF对抗姿势。比如文件上传时改Content-Type、加图片头、双扩展名XSS时用HTML实体编码、Unicode混淆、事件属性拆分。它们背后的思路和SQL注入绕WAF是相通的先摸清过滤规则再找语法或协议上的等价物。第二章这关练会的一套“探测黑名单、构造等价结构、最终组装payload”的流程往后遇到任何带WAF的靶场题都能复用。我个人在攻克封神台第二章时最大的体会是WAF绕过不是靠运气而是靠对SQL语法和过滤规则的双重理解。当你看到某个payload能绕过去时不要只记住它而要把“为什么它能活下来”拆明白下一次换一个过滤规则时你才能真正做到举一反三。
网站建设高端定制企业官网