WAF防护下SQL注入绕过实战:当select与union被过滤后的渗透测试思路

发布时间:2026/8/14 19:06:53
WAF防护下SQL注入绕过实战:当select与union被过滤后的渗透测试思路 1. 从一次真实的渗透测试说起当常规注入手法全部失效最近在做一个授权测试项目时遇到了一个让我印象深刻的场景。目标是一个典型的Web应用登录接口我习惯性地丢了个单引号‘进去页面直接返回了一个醒目的红色警告“检测到非法输入”。换了几种基础的联合查询Union SelectPayload无一例外全部被拦截。这显然不是简单的错误回显被关闭而是部署了Web应用防火墙WAF——它像一道智能闸门精准地识别并阻断了我的试探。这让我想起了很多安全从业者尤其是刚入门的朋友常有的一个误区学会了‘ or 11--和union select 1,2,3就觉得掌握了SQL注入的精髓。实际上在如今WAF无论是云WAF还是硬件WAF基本成为企业标配的背景下这些“教科书式”的Payload存活率极低。WAF的核心策略之一就是针对select、union这类在SQL注入中扮演“骨架”作用的关键字进行高强度过滤。你的攻击流量在到达数据库之前就已经被这道关卡无情地丢弃了。所以我们今天要深入探讨的绝不仅仅是几个“绕过Payload”的罗列。而是要从WAF的检测原理出发理解它为什么能拦截再基于这些原理系统地构建我们的绕过思路。核心目标很明确当select和union这两个最常用的“武器”被明令禁止时我们如何利用数据库特性、协议特性、甚至是WAF规则本身的“盲点”重新打通从注入点到数据泄露的路径。这更像是一场思维的博弈而不仅仅是技术的堆砌。2. 理解对手WAF如何识别与过滤select/union在思考如何绕过之前我们必须先站在防守方的角度弄清楚WAF通常是怎么工作的。虽然不同厂商的WAF规则集Rule Set千差万别但其核心检测逻辑可以归纳为几个层次理解这些层次就等于拿到了绕过之门的钥匙。2.1 基于正则表达式的模式匹配这是最基础、也最普遍的检测方式。WAF会维护一个庞大的恶意模式库其中必然包含针对select、union等关键词的正则表达式。这些正则可能非常“贪婪”例如简单的单词匹配/\bselect\b/i/\bunion\b/i。这里的\b表示单词边界i表示不区分大小写。组合匹配/union\sselect/i用于匹配“union select”这个常连用的攻击模式。上下文感知可能会结合前后字符判断关键词是否出现在疑似SQL语句的上下文中例如跟在单引号、括号后。这种方式的优点是速度快覆盖广。但缺点也很明显规则是死的而HTTP请求和SQL语句的“形态”是活的。只要我们构造的Payload不匹配这些预设的正则模式就有可能穿透。2.2 语义分析与语法树解析更高级的WAF会尝试对输入进行“理解”。它们可能内置一个轻量级的SQL解析器对输入字符串进行词法分析和语法分析尝试构建一个抽象的语法树AST。如果解析器发现输入中包含了SELECT语句的语法结构但上下文比如这是一个登录名的参数又极不正常就会判定为攻击。例如对于admin‘ union select 1,2,3--解析器能识别出这是一个由两个SELECT子句通过UNION连接的合法从语法角度看SQL语句片段从而触发告警。这种方式的绕过难度显著增加因为它不再是简单的字符串匹配而是触及了“意图”层面。2.3 规范化与混淆检测攻击者常用的手段是对Payload进行编码、混淆。成熟的WAF会包含一个“规范化”模块。例如URL解码将%55%4E%49%4F%4EUNION的URL编码还原为UNION。HTML实体解码将union还原为union。多重编码检测防止通过多次编码来绕过单次解码。大小写变异检测SeLeCt、uNiOn这种大小写混合在简单正则下可能失效但高级规则会进行大小写归一化转为全大写或全小写后再匹配。这个模块的目的是尽量将攻击者五花八门的“化妆”卸掉还原出其本来面目再交给模式匹配或语义分析模块去判断。2.4 我的实战观察WAF规则的“惰性”与“误伤”在实际测试中我发现一个有趣的现象很多WAF的规则集存在“惰性”。它们可能只检测某些特定位置如参数值或特定格式如完整的union select组合的关键词。同时过于严格的规则又可能导致“误伤”False Positive影响正常业务。因此管理员往往会在安全性和可用性之间做一个平衡这就在规则集里留下了缝隙。我们的绕过很大程度上就是在寻找并利用这些缝隙。3. 绕过策略一同义词与功能替代——没有select也能“查”当select被直接过滤时我们的第一反应不应该是“如何变形select”而是思考“在这个数据库环境中有没有其他方式可以完成数据查询的功能” 答案是肯定的这依赖于对特定数据库特性的深入了解。3.1 利用HANDLER语句MySQL特定这是MySQL中一个较少被提及但功能强大的语句。HANDLER用于直接访问表的存储引擎接口可以逐行读取数据完全绕开SQL解析器的SELECT语法。更重要的是绝大多数WAF的规则集根本不会包含对HANDLER的检测。基础用法HANDLER table_name OPEN; -- 打开一个表的句柄 HANDLER table_name READ FIRST; -- 读取第一行 HANDLER table_name READ NEXT; -- 读取下一行 ... -- 持续读取直到无数据 HANDLER table_name CLOSE; -- 关闭句柄注入利用场景示例假设注入点位于id参数原查询为SELECT * FROM articles WHERE id‘[INPUT]‘。 我们可以构造-1‘; HANDLER users OPEN; HANDLER users READ FIRST;--执行后会先因id-1使原查询无结果然后执行HANDLER语句读取users表的第一行数据。数据会随着结果集返回只是字段名可能是默认的。你需要通过多次READ NEXT来遍历数据。注意HANDLER操作需要对应表的SELECT权限。但它不通过常规的SQL解析路径因此是绕过SELECT关键字过滤的利器。它的缺点是操作略显繁琐不适合一次性获取大量数据但在关键时候能出奇制胜。3.2 使用PROCEDURE ANALYSE()泄露数据MySQLPROCEDURE ANALYSE()是MySQL用于分析查询结果并给出列优化建议的一个子句。它有一个副作用会将查询结果中的样本数据作为分析建议的一部分返回。利用方法配合子查询即使外层SELECT被过滤我们也可以在子查询或特定上下文中使用它。 例如在可以控制ORDER BY或LIMIT后子句的注入点... ORDER BY (SELECT 1 FROM (SELECT * FROM users LIMIT 1) AS t1 PROCEDURE ANALYSE())--PROCEDURE ANALYSE()会尝试分析t1这个临时表即users表的第一行并在错误信息或返回结果中可能包含该行数据的片段。通过分析错误信息或返回内容可以逐步提取数据。这种方法通常用于基于错误Error-Based的注入场景。3.3 系统视图与函数跨数据库不同的数据库提供了丰富的系统视图和函数来查询元数据有时这些查询方式可以避开对SELECT的简单过滤如果过滤得不彻底。SQL Server: 可以利用sys.tables,sys.columns等视图但通常还是需要SELECT。更可行的思路是利用xp_cmdshell或sp_configure等存储过程进行操作系统命令执行实现曲线救国但这已超出纯数据查询范畴。PostgreSQL: 有pg_tables,pg_stat_user_tables等系统视图。Oracle: 有ALL_TABLES,USER_TAB_COLUMNS等。关键在于WAF可能只过滤应用程序业务逻辑中常用的SELECT语句而对访问这些系统视图的特定语句模式检测较弱。你可以尝试将SELECT与这些生僻的系统视图名组合有时能绕过基于常见表名如users,admin的关联规则。4. 绕过策略二编码、混淆与分割——让WAF“看不清”如果功能替代行不通我们就需要正面突破对select和union这两个词本身进行“化妆”让WAF的检测引擎认不出来。这里的手法非常多样核心思想是破坏正则匹配模式同时保证数据库引擎最终能正确解析。4.1 各类编码技术URL编码最基础但多数WAF会解码。可尝试双重URL编码或非标准编码。union-%75%6e%69%6f%6e(标准URL编码)%75%6e%69%6f%6e-%25%37%35%25%36%65%25%36%39%25%36%66%25%36%65(对%75等符号再次编码)。部分WAF可能只做一次解码。使用非标准编码如将u编码为%u0075(Unicode URL编码)但并非所有环境和数据库都支持。HTML实体编码在Web上下文中有奇效。union-union或union十进制。如果参数值经过Web框架的HTML解码层可能会被还原。十六进制编码MySQL等数据库支持十六进制字符串。select-0x73656c656374在注入中可这样使用‘ and 1(updatexml(1,concat(0x7e,(substring((0x73656c65637420757365722829),1,32))),1))--这里0x73656c65637420757365722829就是select user()的十六进制形式。WAF的正则很可能无法识别这种形态。Unicode/宽字符编码利用数据库对字符集的特殊解析。在GBK等宽字符集中‘单引号的编码是%27如果在其前面加一个ASCII码大于128的字符如%df可能形成%df%27而%df%5c在某些解析中会被当作一个宽字符从而“吃掉”掉转义符\%5c导致单引号逃逸。虽然这主要用于引号逃逸但有时会影响后续关键字的识别上下文。4.2 语法混淆技巧内联注释MySQL/*! ... */是MySQL的特有注释其中的内容会被MySQL服务器执行但其他工具可能将其视为注释。/*!50000union*/ select 1,2,3/*!union*/ select 1,2,3数字50000表示当MySQL版本大于等于5.00.00时才执行其中的语句可用于绕过一些简单的过滤。空白符替代union select中的空白可以用多种字符替代换行符%0a(LF),%0d(CR)制表符%09(TAB)注释union/**/select括号union(select(1),2,3)在某些上下文如子查询中括号可以充当分隔符。这些方法可以破坏/\bunion\sselect\b/这类正则。关键字分割将关键字拆分成多个部分利用数据库的字符串连接功能。MySQL:concat(‘sel‘,‘ect‘)SQL Server: ‘sel‘‘ect‘然后通过动态执行如EXEC(‘...‘)in SQL Server或放在特定函数中执行。例如在SQL Server中; EXEC(‘sel‘‘ect * from users‘)--。这需要SELECT出现在一个字符串中然后被EXEC执行绕过了对静态SELECT的匹配。大小写随机化与等价符UnIoN SeLeCtMySQL中反引号可以包裹标识符在某些上下文中可以尝试union但通常对关键字无效。更有效的是利用和||作为AND、OR的替代但这属于逻辑操作符的绕过。4.3 我的踩坑记录混淆的“度”与数据库版本兼容性我曾经在一个项目里为了绕过WAF构造了一个极其复杂的Payload混合了内联注释、换行符和URL编码。在测试环境MySQL 5.7中完美执行并获取了数据。但一到生产环境MySQL 8.0同样的Payload却导致了语法错误。排查后发现是某个内联注释/*! ... */的用法在8.0中语义发生了细微变化。教训混淆和编码不是越复杂越好。首先要确保你的Payload在目标数据库版本和具体上下文中是语法正确的。最好的方法是在本地或可控环境搭建一个与目标尽可能相似的环境包括数据库类型、版本、Web服务器先验证Payload的语法正确性再测试其绕过WAF的能力。否则你看到的拦截可能是WAF的功劳也可能是你Payload本身就有语法错误。5. 绕过策略三协议与上下文利用——在WAF的盲区起舞有些绕过手法跳出了对Payload字符串本身的纠缠而是利用HTTP协议的特性、应用程序的处理逻辑或者WAF部署架构上的盲点。5.1 HTTP参数污染HPPWAF通常只检查单个参数的值。但如果应用程序后端如PHP的$_GET、$_REQUEST或某些框架的解析方式在接收到多个同名参数时处理方式与WAF不同就可能产生绕过。场景参数id存在注入。正常请求/page.php?idunion select 1,2,3(被WAF拦截)HPP攻击请求/page.php?idunion/*id*/select 1,2,3WAF可能只检查第一个idunion/*或最后一个id*/select 1,2,3两者单独看都不构成完整的union select因此放行。而后端PHP的$_GET[‘id‘]可能会将多个id参数的值用逗号连接或者只取最后一个值取决于配置。如果后端是Tomcat/JSP默认会取第一个值。你需要了解目标后端的技术栈。更常见的是利用分隔符/page.php?id1/*id*/union/*id*/select 1,2,3将完整的Payload拆散到多个同名参数中。5.2 请求方法转换与内容类型GET/POST转换WAF可能对GET请求的参数检测严格但对POST请求的body体检测宽松或者反之。如果一个功能点同时支持GET和POST尝试切换请求方法。Content-Type混淆将Content-Type从application/x-www-form-urlencoded改为multipart/form-data。这两种格式解析参数的方式不同。WAF可能对后者的解析支持不完善导致无法正确提取参数进行检测。尝试text/plain或application/json。如果应用程序意外地支持这些格式并解析了参数而WAF规则没有覆盖则可能绕过。5.3 分块传输编码Chunked Transfer Encoding这是针对那些能够“流式”解析HTTP请求体的WAF的一种高级绕过技术。通过使用HTTP/1.1的Transfer-Encoding: chunked头可以将请求体分块发送。一些WAF为了性能可能只检查第一个数据块或者需要等待整个请求体接收完毕才能分析。攻击者可以构造这样的请求第一个数据块发送无害的内容。第二个数据块发送恶意的SQL注入Payload。 如果WAF基于第一个数据块就做出了“放行”的判断恶意Payload就能抵达后端服务器。实施这种攻击需要手动构造原始的HTTP请求通常借助Burp Suite的Repeater工具并手动编辑HTTP头和数据块。这对攻击者的要求较高且并非对所有WAF都有效。5.4 利用应用程序本身的过滤与还原逻辑这是一种“借力打力”的思路。如果应用程序在接收到参数后会先进行一些处理如解码、替换、过滤然后再拼接到SQL语句中而WAF检查的是处理前的原始参数那么就有可能构造一个Payload使其在WAF看来是无害的但经过应用程序处理后变成了恶意的。经典案例双重URL解码绕过应用程序代码逻辑$id urldecode($_GET[‘id‘]);(进行了一次URL解码)WAF检查原始参数id%2527这是%27的URL编码而%27是单引号‘。过程WAF看到id%2527解码一次得到%27认为这是一个编码后的单引号可能触发拦截取决于规则。但如果WAF只做一次解码它看到的是%2527可能不认为这是一个单引号因为%25是%符号于是放行。请求到达应用程序代码执行urldecode(‘%2527‘)第一次解码得到%27但代码逻辑里只有一次解码吗不urldecode函数会对%27再次解码最终得到‘单引号结果单引号成功注入。对于关键字也可以类似操作例如将union编码为%2575%256e...对%75%6e...再次编码。6. 实战串联一个完整的绕过案例推演让我们假设一个相对复杂的场景将上述多种策略串联起来。目标一个搜索功能参数keyword存在注入后端数据库是MySQL 5.7部署了某云WAF已知其过滤了union和select大小写不敏感并对常见编码做了检测。第一步信息探测提交keywordtest‘返回数据库错误MySQL确认注入点及数据库类型。提交keywordtest‘ and ‘1‘‘1和keywordtest‘ and ‘1‘‘2页面有明显差异确认为布尔盲注。尝试keywordtest‘ union select 1,2,3--页面返回WAF拦截页面。第二步尝试简单混淆keywordtest‘ /*!50000union*/ /*!50000select*/ 1,2,3-- 依然被拦截。说明WAF可能识别了内联注释或者检测到了数字1,2,3这种模式。keywordtest‘ uni/**/on sel/**/ect 1,2,3-- 被拦截。说明/**/注释被正常解析或检测。第三步尝试编码与分割keywordtest‘ and 1(select 1) 被拦截因为包含select。说明对select的检测是独立的。keywordtest‘ and 1(sel‘‘ect 1) 语法错误MySQL不适用连接应用concat。转换思路放弃union采用基于布尔的逐位探测但需要替代select。构造Payload利用substring和ascii函数但需要从某个表查询数据。这里我们尝试用HANDLER语句作为数据源不行HANDLER不返回可直接用于比较的结果集。我们需要一个能返回标量值的“查询”。利用PROCEDURE ANALYSE()进行错误注入先获取表名。假设通过其他信息推断或字典爆破怀疑存在users表。构造Payloadkeywordtest‘ and updatexml(1, concat(0x7e, (select table_name from information_schema.tables where table_schemadatabase() limit 0,1)), 1)--这会被拦截因为包含select。改造尝试将select关键字十六进制编码并嵌入到一个能触发错误执行的函数中。但updatexml第二个参数需要是select语句的执行结果我们不能直接传递编码后的字符串。换一种方法利用exp()函数溢出报错但报错信息中需要包含查询结果这又绕不开select。第四步深入利用数据库特性——无select查询回忆MySQL的HANDLER。虽然不能直接用于布尔比较但可以用于时间盲注构造时间延迟Payloadkeywordtest‘ and if((substr(user(),1,1)‘r‘), sleep(5), 0)-- 被拦截可能检测了sleep或函数组合模式。使用HANDLER配合BENCHMARK或繁重运算实现时间延迟思路如果某个条件为真就执行一个非常耗时的HANDLER操作。但HANDLER本身不返回布尔值无法用在if里。我们需要一个能返回标量值的“条件判断”。回到原点布尔盲注的核心是比较。我们能否不通过select获取比较值比如利用数据库内置变量或函数灵光一现‘ and ‘abc‘(‘a‘||‘b‘||‘c‘)--在MySQL中||是逻辑或不是连接符所以不行。concat(‘a‘,‘b‘,‘c‘)需要select。利用like和%通配符进行模糊匹配这需要已知数据的一部分。第五步迂回策略——联合查询的替代方案既然union select被盯死我们是否可以不用联合查询而用其他方式获取数据对于布尔盲注我们确实可以不用union但必须用select来从表中取数据。如果select被过滤布尔盲注和报错注入的常用Payload都难以构造。 此时需要考虑是否过滤了其他关键函数如substring,ascii,mid,left等。如果这些函数可用我们可以尝试一种极其迂回的方式利用load_file()函数读取文件内容但这需要绝对路径和文件读取权限。利用into outfile或dumpfile写Webshell但这需要写权限和已知Web路径。 这两种方式都严重依赖于环境配置通用性不强。第六步回归基础——细微处见真章在多次尝试复杂绕过失败后我决定回归最简单的Payload但在细节上做文章。尝试超长数据绕过有些WAF对单个参数值有长度限制超过部分不检查。我构造了一个超长的keyword参数前面是大量填充字符如几千个A最后附上‘ union select 1,2,3--。结果被拦截。说明WAF处理了长参数。尝试参数污染HPP请求/search?keywordtest‘ /*keyword*/union/*keyword*/select 1,2,3--观察后端如何处理多个keyword。通过响应差异判断发现后端似乎只取了第一个keyword的值test‘ /*。失败。尝试换行符分割在Burp Suite中直接修改Raw请求在union和select之间插入一个换行符%0a。GET /search?keywordtest‘ union%0aselect 1,2,3-- HTTP/1.1奇迹发生了页面正常返回并且显示了2和3的位置这意味着这个WAF用于检测union select的正则规则很可能没有考虑换行符作为空白符的情况。\s通常匹配空格、制表符等但某些正则编写时可能用了[ ]或没有包含\n。最终Payload 通过使用%0a换行符成功分隔了union和select绕过了WAF的检测。后续的数据获取就顺理成章了keywordtest‘ union%0aselect 1,database(),3--获取数据库名。keywordtest‘ union%0aselect 1,table_name,3 from information_schema.tables where table_schemadatabase() limit 0,1--获取表名。... 依次获取字段名和数据。这个案例告诉我们最复杂的混淆未必是最有效的。有时一个最简单的协议级字符如换行符、制表符就能击穿WAF的规则。关键在于系统地尝试并且对HTTP协议和正则表达式有基本的理解。在实战中我通常会准备一个包含各种分隔符、编码变形的测试字典用Intruder等工具进行模糊测试效率远比手动尝试高得多。