SQL 注入系统学习(二):联合查询与元数据
本文仅用于 CTF、靶场、本机实验和明确授权环境。
上一篇解决的是“输入进入了什么 SQL 上下文”。这一篇继续处理有数据回显时最常见的路线:联合查询。
联合查询不是从 UNION SELECT 开始,而是先回答三个问题:原查询有几列、哪些列能承载目标数据、哪些列会显示在页面上。
1. UNION 的基本要求
假设原查询是:
1 | SELECT id, username |
联合另一条查询:
1 | SELECT id, username FROM users WHERE id = 1 |
通常需要:
1 | 两边列数相同 |
因此,直接输入一长串 UNION SELECT 后报错,并不能判断漏洞是否存在。
2. 判断列数
2.1 ORDER BY
在本地靶场中逐步测试:
1 | ORDER BY 1 |
假设前两个正常,第三个出现“未知列位置”一类错误,可以推断结果集大概率只有两列。
常见失败原因:
1 | 错误被应用隐藏 |
2.2 使用 NULL 占位
1 | UNION SELECT NULL |
NULL 能与多种数据类型兼容,适合先判断列数。列数正确但页面仍没有变化,还需要继续找回显位。
3. 寻找回显位
确定有三列后,可以在本地靶场使用明显的标记:
1 | UNION SELECT 111,222,333 |
如果页面显示 222,说明第二列可能进入了页面模板。
但要注意:
- 应用可能只读取第一行;
- 页面可能只读取固定字段名;
- HTML 转义可能改变显示;
- 原查询结果可能排在联合结果之前;
- 某些列只能接受数字或日期。
4. 原查询结果干扰
假设页面只展示第一行,原查询又确实命中记录,那么联合出来的数据可能根本看不到。
解决方向不是背一个固定前缀,而是让原查询条件为空:
1 | 原条件返回 0 行 |
如何让原条件为空,取决于数字型、字符串型、LIKE 或括号上下文。
5. 常用 MySQL 信息
1 | DATABASE() |
其中:
1 | DATABASE() / SCHEMA() 当前数据库 |
USER() 和 CURRENT_USER() 语义不同,分析权限时不要完全混用。
6. information_schema
MySQL 常通过 information_schema 暴露数据库对象的元数据。
6.1 数据库名
1 | SELECT schema_name |
6.2 当前数据库的表名
1 | SELECT table_name |
6.3 指定表的字段名
1 | SELECT column_name |
6.4 为什么枚举不到
1 | 当前账户可见范围有限 |
看到 information_schema 失败,不应立即认定没有注入。
7. GROUP_CONCAT:把多行压成一行
1 | SELECT GROUP_CONCAT(table_name) |
指定分隔符:
1 | SELECT GROUP_CONCAT(table_name SEPARATOR 0x7c) |
0x7c 是字符 | 的十六进制表示。
限制:
1 | 受 group_concat_max_len 影响 |
8. GROUP_CONCAT 被过滤
它承担的能力是“把多行结果合并成一行”。替代方法要围绕这个目标选择:
1 | SELECT table_name |
改变 OFFSET 可以逐行读取。
其他路线:
1 | 目标表名已知 → 跳过全库枚举 |
9. UNION 被过滤
先判断过滤性质:
1 | 大小写敏感匹配? |
如果联合查询通道确实不可用,应切换反馈通道:
1 | 错误回显 |
无限变形 UNION 并不是唯一路线。
10. 逗号被过滤
联合查询的多列天然需要逗号。可考虑:
1 | 原查询是否只有一列 |
函数参数中的逗号有时能用另一种语法替代,但多列列表并没有普遍适用的无逗号写法。
11. 防御
数据值使用参数化查询;动态列名、表名和排序方向使用固定映射。数据库账户遵循最小权限,生产环境不向页面返回完整数据库错误。
12. 本篇速查
1 | 确认回显 |
下一篇整理 MySQL 常见函数,以及函数、逗号、引号或比较符被过滤时如何按“能力”寻找替代。