软件安全 --- CTF比赛 之 SQL union注入
做 CTF 时,可以统一记住这条主线:
信息收集 → 判断注入点 → 闭合 SQL → 确定列数 → 定位回显位 → 获取数据库信息 → 枚举表和列 → 提取 Flag → 整理 WriteUp。
以下步骤适用于授权的 CTF 靶场和本地实验环境。
一、SQL 注入通用答题流程
01 · 信息收集
请求方式、参数名、正常响应、后端指纹
02 · 判断注入点
单引号、括号、布尔条件、错误差异
03 · 确定 SQL 结构
找到闭合方式,确定注释符与列数
04 · 确定 UNION 回显位置
用不同数字标记每一列,观察页面输出
05 · 枚举数据库结构
数据库 → 数据表 → 字段
06 · 提取并验证 Flag
读取目标字段,记录证据与完整利用链
第一步:信息收集
拿到题目后,先不要急着构造复杂的注入语句。
重点检查以下信息:
|
检查项目 |
需要确认的内容 |
|---|---|
|
题目类型 |
Web、SQL 注入、联合查询等 |
|
请求方式 |
GET 还是 POST |
|
参数名称 |
|
|
正常响应 |
输入合法 ID 后返回什么 |
|
异常响应 |
错误、空白页面、数据变化 |
|
后端指纹 |
Apache、PHP、MySQL 等 |
|
页面回显 |
哪些字段显示在页面上 |
例如,两道题的请求方式不同:
GET /?id=1 HTTP/1.1
另一道题则是通过 POST 表单提交:
POST /index.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded
id=1
关键点: 先确定参数在哪里,再决定怎样测试。GET 和 POST 的注入原理一样,但请求构造方式不同。
第二步:判断是否存在 SQL 注入
依次测试三类输入。
1. 特殊字符测试
输入单引号 '、双引号 ",观察页面是否出现错误、空结果或响应变化。
目的:判断参数可能处于哪种 SQL 语法上下文。
2. 布尔条件测试
在合法的注入语法中分别构造恒真条件与恒假条件,比较响应是否存在稳定差异。
目的:确认输入是否影响 SQL 查询逻辑。
3. 闭合方式测试
判断参数是否被单引号、双引号或括号包裹,以及需要怎样结束当前表达式。
目的:推断后端 SQL 语句的实际结构。
常见 SQL 上下文
|
后端 SQL 形式 |
参数后需要考虑的结构 |
|---|---|
|
|
字符串单引号 |
|
|
字符串单引号和右括号 |
|
|
数值表达式,通常没有引号需要闭合 |
以第二道题为例,已知页面显示的 SQL 类似:
SELECT * FROM news WHERE id=('$id')
因此,不能只闭合单引号,还要处理圆括号。
核心原则:先判断原始 SQL 上下文,再构造注入语句。不要把所有题目都套用同一个 payload。
另外,MySQL 中 -- 注释通常要求后面跟空白字符;# 也可以作为行注释符,但在 GET URL 中应注意 URL 编码,避免浏览器把它当成片段标记。
第三步:确定查询列数
这是 UNION 注入中最重要的步骤之一。
UNION SELECT 两侧的查询必须返回相同数量的列,否则通常会报错。
常见方法有两种:
|
方法 |
操作思路 |
判断依据 |
|---|---|---|
|
|
逐步增加排序列索引 |
超出列数时通常报错 |
|
|
逐步增加 SELECT 表达式数量 |
列数一致时才满足联合查询的结构要求 |
例如,假设已经在本地靶场确认了正确的闭合方式,可以逐步测试:
ORDER BY 1
ORDER BY 2
ORDER BY 3
ORDER BY 4
如果排序到第 4 列仍然有效,而第 5 列失败,则可以初步判断原查询有 4 列。
再通过列数匹配的 UNION 查询进行交叉验证。
两道题的结果分别是:
|
题目 |
原查询列数 |
|---|---|
|
学生成绩查询系统 |
4 |
|
where is flag |
2 |
注意:单次请求没有返回结果不一定意味着列数错误,也可能是闭合失败、语法错误或应用隐藏了错误信息。
第四步:确定哪些列能够回显
确定列数后,下一步不是直接读取 Flag,而是先确认页面展示了哪几列。
原理是给不同列设置不同的标记值,例如:
SELECT 111, 222, 333, 444
在授权靶场中,将其放入正确的 UNION 查询结构,再观察页面输出。
假设结果显示:
Math: 222
English: 333
Chinese: 444
就说明第 2、3、4 列能够显示,而第 1 列可能被用于标题、表头或其他逻辑。
需要特别留意两个问题:
-
原查询结果干扰: 如果原查询已有匹配记录,页面可能优先展示原始结果。应在靶场中确认怎样让原查询不返回记录。
-
只取第一行: 如果后端只读取查询结果的第一行,UNION 添加的行未必能显示出来。
因此,必须先弄清楚页面的真实回显逻辑,再选择适合的回显列。
两道题的差异:
|
项目 |
成绩查询系统 |
where is flag |
|---|---|---|
|
查询列数 |
4 |
2 |
|
可用回显位置 |
第 2~4 列 |
第 1、2 列 |
|
推荐关注位置 |
第 2 列 |
第 2 列(Data) |
第五步:获取数据库基本信息
确认 UNION 注入有效、列数正确且回显位置明确之后,才进入数据库信息收集阶段。
MySQL 常见信息函数:
|
函数或变量 |
作用 |
|---|---|
|
|
查看数据库版本 |
|
|
查看当前数据库名称 |
|
|
查看当前连接使用的数据库账户 |
|
|
查看数据库数据目录 |
例如,第二道题中获得的信息包括:
-
数据库版本:MySQL 5.5.50
-
当前数据库:
sqli -
当前连接用户:
root@localhost -
数据目录:
/var/lib/mysql/
这些信息可以帮助你判断数据库类型、定位当前库,并评估数据库账户的权限风险。
但要注意:user() 返回的连接账户身份,不一定等同于所有权限检查场景下的有效权限;不能仅凭用户名就断定所有操作都被允许。
第六步:枚举数据库、数据表和字段
这是两道题共同使用的核心技术。
MySQL 提供 information_schema 元数据视图,可以查询数据库结构。
第 1 层:确定数据库
先查看 database() 返回的当前数据库,必要时再检查可见的数据库列表。
第 2 层:查找数据表
使用 information_schema.tables 查询目标数据库中的表名。
SELECT table_name
FROM information_schema.tables
WHERE table_schema = '目标数据库';
第 3 层:查找字段
使用 information_schema.columns 查询目标表有哪些字段。
SELECT column_name
FROM information_schema.columns
WHERE table_schema = '目标数据库'
AND table_name = '目标表';
第 4 层:读取目标字段
确认字段名后,在授权靶场中读取对应数据。
SELECT 目标字段 FROM 目标表 LIMIT 0,1;
两道题的枚举结果:
|
项目 |
第一题 |
第二题 |
|---|---|---|
|
当前数据库 |
|
|
|
目标表 |
|
|
|
目标字段 |
|
|
第一题通过逐条查询元数据找到 fl4g.flag;第二题则找到 flag.flag。
通用口诀:库 → 表 → 列 → 值。
不要一开始就假定 Flag 一定存放在名为 flag 的表里。有些题目会使用其他表名、字段名,甚至把目标内容藏在普通业务数据中。
第七步:读取并验证 Flag
当数据库、数据表、字段和回显位置都确认后,最后才读取目标值。
在这两道题中,最终得到的结果分别是:
第一题:学生成绩查询系统
flag{bab860507ae7707cf9a18883804d248c}
第二题:where is flag
flag{c85090142dafdc10e1bf4e43daa385df}
拿到 Flag 后,还需要核对:
-
格式是否符合题目要求。
-
是否完整复制,没有多余字符或空格。
-
是否确实来自目标字段,而不是页面中的普通文本。
-
是否保留了从注入点到最终结果的关键证据。
二、常见问题排查表
CTF 中最容易浪费时间的情况,并不是不知道 UNION,而是前面的某个判断出了问题。
|
现象 |
优先检查什么 |
|---|---|
|
单引号后页面空白 |
SQL 语法是否被破坏,应用是否隐藏错误 |
|
恒真和恒假没有区别 |
闭合是否正确、响应差异是否可靠 |
|
|
原始 SQL 结构、注释和参数类型 |
|
UNION 一直失败 |
列数是否匹配,表达式类型是否兼容 |
|
UNION 成功但没有目标数据 |
回显位置、原查询结果、首行读取逻辑 |
|
能显示数字但不能显示字符串 |
字段类型、输出处理和表达式位置 |
|
枚举不到目标表 |
当前数据库是否正确、表名是否猜错、权限是否足够 |
|
页面只显示部分结果 |
是否只取第一行、输出标签是否异常、结果长度是否受限 |
特别是这两道题,都体现了一个容易忽略的问题:SQL 查询成功,不代表查询结果一定会显示在页面上。
数据库执行逻辑和应用层展示逻辑,需要分别判断。
三、CTF 答题记录模板
每次做 SQL 注入题,都可以按下面的结构记录。这样不容易遗漏步骤,也方便后续整理 WriteUp。
答题检查清单
0/9 已完成
1. 信息收集
请求方法、参数名、页面正常响应、后端指纹
2. 注入点确认
特殊字符测试、恒真/恒假对比
3. 闭合方式确认
引号、括号、注释符及 SQL 上下文
4. 列数确认
ORDER BY 探测与 UNION 列数验证
5. 回显位置确认
不同标记值、原查询结果与首行逻辑
6. 数据库信息
数据库版本、当前库、当前连接用户
7. 结构枚举
数据库、数据表、字段名
8. 数据读取
目标字段、返回内容、Flag 格式验证
9. 复现与修复
关键请求、响应证据、根因与修复建议
四、最后记住这几个原则
-
先判断上下文,再构造语句。 不同题目的引号、括号和参数类型可能不同。
-
先确定列数,再寻找回显位。 UNION 查询必须与原查询列数匹配。
-
先找库表字段,再读取数据。 不要凭经验假定表名或字段名。
-
区分数据库执行和页面显示。 原查询是否返回记录、后端是否只取第一行、哪些字段被展示,都会影响结果。
-
把证据写完整。 每一步记录输入、响应和结论,最终 WriteUp 才有复现价值。
从防御角度看,最重要的修复方式是使用 PDO 或 MySQLi 预处理语句,避免把用户输入直接拼接进 SQL;再配合输入白名单、最小数据库权限和关闭生产环境的详细错误输出。仅靠 intval()、隐藏报错或 WAF 都不能代替参数化查询。
浙公网安备 33010602011771号