
联合注入是SQL注入里最直观、最容易出效果的一种手法也是几乎所有自动化工具默认会先尝试的路径。但正因为它太经典防御方对它的过滤也最严——你输入union select拦的就是这两个词。这篇文章我会从实际测试中总结的经验出发拆解常见的过滤逻辑再逐一演示对应的绕过思路最后聊一聊为什么理解这些技巧对防守方同样重要。1. 先搞清楚防线在哪里常见的联合注入过滤实现在我接触过的项目里针对联合注入的过滤基本分成三个层次。搞清楚自己面对的是哪一类才能选对绕过的方向。第一类是代码层的黑名单过滤。业务代码里用preg_replace或者str_replace把union、select这些关键字直接替换成空字符串或者替换成无害字符。这种过滤最简单粗暴但也最容易被绕过——因为它的规则是死的没有上下文理解能力。第二类是Web应用防火墙WAF层面的正则检测。这类过滤比代码层复杂一些通常会看完整的攻击载荷匹配类似union\sselect这样的组合模式。有些规则写得好会考虑各种变形和编码但多数情况下规则写得比较宽反而留出了钻空子的空间。第三类是数据库层面的限制。比如某些数据库中间件会限制SQL语句长度或者过滤掉注释符、特殊空白符。这类限制通常不是专门针对联合注入但会间接影响注入载荷的构造方式。测试的第一步是确定过滤到底拦了什么、放行了什么、命中后返回什么样的特征。比如有一次我遇到一个报错型拦截输入union直接返回500输入UNION大小写变体却正常返回——这说明规则写得死板只匹配了小写形式。判断清晰了绕过方案就明确了一半。2. 联合注入绕过实战从基础到进阶的八种技巧绕过的本质就是让最终到达数据库解析器的语句语义不变但形式不同从而避开检测规则。下面这些方法我从易到难排列各自适用不同的过滤场景。2.1 大小写变形与关键字重写这是最基础但效率很高的一招。很多过滤规则只匹配union select的全小写形式或者只匹配标准的union select组合。测试方法很简单原始载荷 union select 1,2,3 大小写变形 Union Select 1,2,3 混合变形 UnIoN sElEcT 1,2,3还有一种做法是用等价的关键字替换。比如MySQL里union all和union distinct都合法如果规则只拦了union select试试union all select或者反过来。我遇到过规则拦截了UNION后面的空白符但没拦UNION ALL的直接就绕过去了。再比如默认的select被过滤了可以用select的替代写法——MySQL支持SELECT语句里的TABLE关键字吗实际上在MySQL 8.0.19之后确实可以用TABLE t来替代SELECT * FROM t这个技巧在特定版本下很实用但兼容性要提前测。2.2 注释符切割关键字注释符绕过的思路是在关键字中间插入注释让过滤正则匹配失败但数据库解析时会把注释内容忽略掉拼出来的语句语义不变。常用的是内联注释原始载荷 union select 1,2,3 注释切割 un/**/ion sel/**/ect 1,2,3这个手段对很多正则过滤有效因为正则匹配union时中间的/**/导致匹配失败。MySQL解析器在处理/**/时会当作空白或者直接忽略拼接后实际执行的还是union select 1,2,3。注意一个细节注释符绕过对#和--这种行注释无效因为它们会注释掉后面所有的SQL语句。只有/**/这种块注释能在语句中间安全使用。2.3 空白符替代策略过滤规则里常见union\sselect这种写法\s匹配的是标准空白符也就是空格、制表符、换行符。如果绕过\s的匹配范围就能让正则失效而数据库仍然能正常解析。MySQL里可以替代空白的字符比想象中多包括%09 Tab水平制表符 %0A 换行 %0B 垂直制表符 %0C 换页 %0D 回车 %0E MySQL扩展 %0F MySQL扩展 %a0 非ASCII空格某些版本生效实际上我测试过多个版本最稳的组合是%0A和%0B它们在MySQL的tokenizer里都能被识别为空白分隔符。构造出来是这样的原始载荷 union select 1,2,3 空白替代 union%0Bselect%0B1,2,3如果纯手工测试不方便直接放进Burp的Repeater里用CtrlShiftU做URL编码再手动改编码值就行。2.4 内联注释与版本关键字组合MySQL有一种特别的内联注释语法格式是/*!50000 ... */里面内容即使被注释标记包裹也会被执行——只要版本号条件满足。很多过滤规则对/*!不敏感但它内部的内容会被数据库正常解析。比如原始载荷 union select 1,2,3 内联版本注释 /*!union*/ /*!select*/ 1,2,3这个技巧绕过了大量按关键字简单匹配的规则因为过滤逻辑看到的是注释符包裹的内容而数据库执行时看到的是完整的union select。我实测过这类技巧对规则写得比较简单的WAF几乎必中但对启用了语义分析的WAF就不好说了。这类WAF会先解码再匹配注释符会被剥掉这时候就得回到编码层面对抗。2.5 十六进制与URL编码嵌套数据的编码解析是有顺序的Web服务器先做URL解码再传给应用层应用层如果做了二次解码就会产生双重编码的问题。WAF如果只解码一次就看不到真实载荷数据库如果收到了半解码的字符串也解析不了——所以这种绕过对场景要求高只适用特定环境。先看一层编码的情况union 的十六进制 0x7573696f6e select 的十六进制 0x73656c656374在某些拼接场景下这些十六进制会被数据库函数解析回原义。比如注入点在load_file这类函数附近时十六进制值会被当作字符串读取并解码。但要注意的是union和select作为关键字不能被十六进制直接替代后参与语句解析——它们只能在字符串值的位置用。所以这条技巧主要适用于字符串绕过不适用于联合注入的关键字段。相比之下URL编码更适合对付以URL参数为检测对象的WAF。测试时可以逐字符编码先把union整体编码成%75%6e%69%6f%6e如果被拦就再做一层嵌套%2575%256e%2569%256f%256e。但必须确认后端有没有二次解码逻辑否则会得到一条语法错误的SQL。2.6 参数污染与多参数拆分HTTP参数污染HPP的核心是同一个参数名提交多个值不同平台的解析结果不一样。比如?id1idunion select 1,2,3服务器可能只取第一个值、只取最后一个值或者把两者拼接起来。实际操作中我常用到的是把联合注入拆成两段第一段 id1 union 第二段 idselect 1,2,3如果WAF是对每个参数独立检测的单看任何一个参数都检测不到完整的union select组合但如果后端的拼接逻辑是把多个值拼在一起再执行那就会中招。参数污染对依赖参数遍历的WAF效果很好但也取决于框架的取值方式。PHP默认取最后一个值Java的getParameter取第一个而某些网关会把所有值都拼接。测试前先确认后端的取值行为否则就是在碰运气。2.7 数据库差异利用与函数替代不同的数据库之间语法差异本身就是过滤盲区。比如在SQL Server里可以用union all配合null列替代select的列。Oracle里可以通过from dual表构造查询。但如果WAF把select当成通用关键字拦了这些替代就无效。MySQL还有一个技巧union配合数值型列时可以不用select关键字用table直接取值。类似这种版本特性要用版本探测来配合——先报错注入或者version探测数据库版本再针对版本挑选替代方案。还有一种常见做法是用查询子句替代简单字段。比如原始载荷 union select 1,2,3 替代方案 union select * from (select 1) a join (select 2) b join (select 3) c这个方法对过滤union select后面是否跟数字的正则有效因为载荷结构改变了但语义一致。2.8 换行混淆与多级嵌套把注释和编码混用在一条注入语句里面对多层规则的压力是最大的。比如原始载荷 union select 1,2,3 混用变形 %75%6e/**/%69%6f%6e%0b%73/**/e%6cect 1,2,3这样一层层组合下来被逻辑简单的规则误判为正常字符串的概率就很高。我自己测试过很多基于正则匹配的WAF在遇到多级混合编码加注释切割时都会直接放行——因为它们的解码顺序和解码次数和数据库不一致。不过要记住每条注入语句最终是要能被数据库执行的不是单纯为了绕WAF。构造完载荷后一定要在本地数据库里验证语义是否正确避免在目标上试半天却是因为语法错误。3. 实操现场从过滤探测到成功注入的完整步骤单纯列出技巧还不够我分享一下完整的实操流程。从前期的探测到最终拿到数据每一步都有明确的目的和依据。3.1 定位注入点并做探测注入点的确定是第一步也是最容易被跳过的一步。很多新手一上来就想着怎么绕过滤结果注入点本身就是假的——WAF的拦截页面反而让测试者误判了注入状态。我一般是这么做的先用单引号、双引号和逻辑真值1/0做三组基础探测观察返回差异。如果id1和id2返回不同内容说明参数对结果有直接影响如果id1报错或者被拦截说明数据库解析了我们的输入存在注入可能性。之后要用order by判断列数这也是联合注入的前提。从order by 1开始逐步递增直到报错为止报错前的那个数就是列数id1 order by 1 id1 order by 2 id1 order by 3 id1 order by 4 -- 报错说明列数为3如果order by也被过滤了可以用group by替代效果一样。这两种操作符都可能触发过滤但从联合注入的角度看order by的过滤率比group by更高因为它的攻击特征更明显。3.2 判断过滤规则的类型确定是联合注入点后再回头测过滤。我这里有个常用的探测集每一个输入都代表一类过滤规则union select 1,2,3 -- 基础检测 UNION SELECT 1,2,3 -- 大小写检测 union/**/select 1,2,3 -- 注释切割检测 union%0bselect%0b1,2,3 -- 空白替换检测 /*!union*/ /*!select*/ 1,2,3 -- 内联注释检测观察哪个被拦、哪个放行、拦截返回什么状态码基本就能推断出规则的实现方式。有一次我测一个目标小写被拦大写放行这说明规则配置的时候偷懒了——只写了小写匹配。这类细节就是突破口。3.3 依据规则选型并构造载荷确定规则类型之后按优先级从低到高依次试用绕过方案。我的习惯是先试最简单的——大小写变形和数据里自带的大小写混写行不通再用空白符替代再不行就组合注释和编码。举一个实际案例。某次测试中我发现union select被拦但单独输union和单独输select都不拦说明规则是针对组合模式的。我马上用union%0bselect测试结果放行。数据库实际执行时%0b被解析成垂直制表符等同于空格分隔——表达式语义完全不变。拿到放行结果后后续的查询语句也需要保持一致风格。构造出id1%0bunion%0bselect%0b1,2,3然后再根据页面回显位置替换成实际要查的数据字段。3.4 数据提取与输出位置确认联合注入的回显位置很关键。select 1,2,3的每一个数字对应一个回显列要找到页面上哪些位置会输出数据。比如页面正常显示的是id和name字段那注入语句里2和3的位置就会直接展示在页面上。接下来替换成目标数据id1%0bunion%0bselect%0b1,database(),version()如果页面不回显数据库名和版本号就说明回显位置不对或者输出被过滤了。这时候可以考虑用group_concat把查询结果合并到一个字段再输出也可以起子查询嵌套id1%0bunion%0bselect%0b1,(select group_concat(table_name) from information_schema.tables where table_schemadatabase()),3这个过程的每一步都需要确认列数对不对、回显点在哪、目标数据能否输出。任何一步出错都会表现为页面异常或者空回显排查的时候要逐层回退验证。4. 常见问题与排查技巧实录实际操作中遇到的坑比技巧本身更多。下面这些是真实测试中反复出现的典型问题。4.1 回显不存在的处理方式联合注入最尴尬的情况是注入点存在、列数已确定但页面就是不回显数据。这通常说明注入点不在数据输出链路内比如数据被统计后用了聚合或者输出被过滤直接丢弃了。替代方案是用报错注入或者时间盲注。报错注入里用extractvalue或者updatexml这类在MySQL下会报错并携带SQL语句片段的函数id1 and extractvalue(1,concat(0x7e,version()))时间盲注用sleep配合if条件判断逐字符猜数据速度慢但可靠。这套思路对有注入但无回显的目标非常管用。4.2 MYSQL与SQL Server的绕过差异不同数据库在过滤绕过面前的表现差异很大。MySQL能用的/**/内联注释、%0b空白替代在SQL Server里不一定生效。SQL Server的解析器对/**/的支持也不完全相同但%0b相比MySQL的限制更严格。Oracle的处理方式就更特殊了。它不支持%0b替代空白但对注释符的宽容度较高。此外Oracle的字符串连接用||而MySQL默认用concat函数——如果测试目标换成Oracle之前构造的载荷几乎都要重写。测试前的指纹识别比如通过响应头、报错信息确定数据库类型能省去大量无效测试。4.3 WAF误报与真实拦截的区分WAF拦截和业务逻辑报错经常在页面上很相似。我的经验是记录每次请求的状态码和响应体。如果输入明显是SQL语法错误时返回500但输入union select时返回403那基本可以确认是WAF拦截。区分开这两者才能判断是绕WAF的问题还是SQL语句本身的问题。另外拦截触发的页面往往带有WAF的产品特征比如特定的响应头。测试时把这些特征记下来就能确认代理链路上到底有没有WAF在中间。4.4 常见问题速查表问题现象可能原因排查方向输入union即被拦截存在独立关键字黑名单尝试大小写、内联注释拆分输入union select被拦但单独输入放行规则匹配组合模式尝试空白符替代、注释切割回显点有内容但无法输出字段输出过滤或回显位置判断错误改用报错注入或时间盲注所有绕过方式都失败使用了参数化查询或严格语义分析放弃联合注入寻找二次注入点编码后仍被拦截WAF做了解码后再检测尝试双重编码并验证后端解码逻辑5. 过滤绕过背后的防御思路研究绕过技巧的最大价值不是攻击本身而是它暴露了防御方的薄弱环节。我在做代码审计和安全加固时看到很多防御方案的问题其实高度一致。第一依赖黑名单根本不靠谱。黑名单永远列不全所有的攻击变形而且WAF规则更新往往滞后于攻击手法的更新。防护的重点应该放在参数化查询和输入类型校验上——校验id是数字类型就传入参数化查询从源头上消除执行恶意语句的可能。第二WAF规则要覆盖解码层次。很多绕过成功是因为WAF和数据库对输入的处理顺序不一致。防御方必须在同一条链路上模拟数据库的解码逻辑至少要支持一层URL解码后再做规则匹配。第三最小权限原则能大幅降低注入影响。即使注入成功了如果数据库账号权限被限制为只读、只允许返回有限行数数据泄露的威胁也会被限制在可控范围内。防御不是靠堆规则而是要靠架构设计。参数化查询配合最小权限再加一层行为检测型WAF才是真正有效的组合。6. 从实战视角看工具的取舍结合注入绕过实操工具的选型也直接影响效率。我长年用的组合是Burp Suite加手工测试脚本。Burp负责请求的捕获、重放和编码变换拦截响应可以快速观察过滤结果。手工脚本则用来批量生成不同约束条件下的注入载荷加速探测过程。选工具的核心标准不是功能多而是能否灵活控制编码和请求结构。很多工具内置的注入载荷库虽然方便但在过滤规则复杂的时候反而误事——因为无法精确控制某个字符的编码方式。我倾向于用Python写简单的请求脚本把载荷变体和URL编码逻辑完全掌握在自己手里。工具和手法的配合没有绝对最优解核心是每一步操作都能充分理解目的和边界条件。毕竟SQL注入的测试本质上是一场对输入处理链路的逆向推演工具只是辅助推演的工具不是替代思考的捷径。