GKLBB

当你经历了暴风雨,你也就成为了暴风雨

导航

软件安全 --- CTF比赛 之 SQL union注入

 

做 CTF 时,可以统一记住这条主线:

信息收集 → 判断注入点 → 闭合 SQL → 确定列数 → 定位回显位 → 获取数据库信息 → 枚举表和列 → 提取 Flag → 整理 WriteUp。

以下步骤适用于授权的 CTF 靶场和本地实验环境。

一、SQL 注入通用答题流程

01 · 信息收集

请求方式、参数名、正常响应、后端指纹

 

02 · 判断注入点

单引号、括号、布尔条件、错误差异

 

03 · 确定 SQL 结构

找到闭合方式,确定注释符与列数

 

04 · 确定 UNION 回显位置

用不同数字标记每一列,观察页面输出

 

05 · 枚举数据库结构

数据库 → 数据表 → 字段

 

06 · 提取并验证 Flag

读取目标字段,记录证据与完整利用链

第一步:信息收集

拿到题目后,先不要急着构造复杂的注入语句。

重点检查以下信息:

检查项目

需要确认的内容

题目类型

Web、SQL 注入、联合查询等

请求方式

GET 还是 POST

参数名称

id、uid、search 等

正常响应

输入合法 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 形式

参数后需要考虑的结构

WHERE id='$id'

字符串单引号

WHERE id=('$id')

字符串单引号和右括号

WHERE id=$id

数值表达式,通常没有引号需要闭合

以第二道题为例,已知页面显示的 SQL 类似:

SELECT * FROM news WHERE id=('$id')

因此,不能只闭合单引号,还要处理圆括号。

核心原则:先判断原始 SQL 上下文,再构造注入语句。不要把所有题目都套用同一个 payload。

另外,MySQL 中 -- 注释通常要求后面跟空白字符;# 也可以作为行注释符,但在 GET URL 中应注意 URL 编码,避免浏览器把它当成片段标记。

第三步:确定查询列数

这是 UNION 注入中最重要的步骤之一。

UNION SELECT 两侧的查询必须返回相同数量的列,否则通常会报错。

常见方法有两种:

方法

操作思路

判断依据

ORDER BY

逐步增加排序列索引

超出列数时通常报错

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 常见信息函数:

函数或变量

作用

version()

查看数据库版本

database()

查看当前数据库名称

user()

查看当前连接使用的数据库账户

@@datadir

查看数据库数据目录

例如,第二道题中获得的信息包括:

  • 数据库版本: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;

两道题的枚举结果:

项目

第一题

第二题

当前数据库

web1

sqli

目标表

fl4g

flag

目标字段

flag

flag

第一题通过逐条查询元数据找到 fl4g.flag;第二题则找到 flag.flag。

通用口诀:库 → 表 → 列 → 值。

不要一开始就假定 Flag 一定存放在名为 flag 的表里。有些题目会使用其他表名、字段名,甚至把目标内容藏在普通业务数据中。

第七步:读取并验证 Flag

当数据库、数据表、字段和回显位置都确认后,最后才读取目标值。

在这两道题中,最终得到的结果分别是:

第一题:学生成绩查询系统

flag{bab860507ae7707cf9a18883804d248c}

 

第二题:where is flag

flag{c85090142dafdc10e1bf4e43daa385df}

拿到 Flag 后,还需要核对:

  • 格式是否符合题目要求。

  • 是否完整复制,没有多余字符或空格。

  • 是否确实来自目标字段,而不是页面中的普通文本。

  • 是否保留了从注入点到最终结果的关键证据。

二、常见问题排查表

CTF 中最容易浪费时间的情况,并不是不知道 UNION,而是前面的某个判断出了问题。

现象

优先检查什么

单引号后页面空白

SQL 语法是否被破坏,应用是否隐藏错误

恒真和恒假没有区别

闭合是否正确、响应差异是否可靠

ORDER BY 一直报错

原始 SQL 结构、注释和参数类型

UNION 一直失败

列数是否匹配,表达式类型是否兼容

UNION 成功但没有目标数据

回显位置、原查询结果、首行读取逻辑

能显示数字但不能显示字符串

字段类型、输出处理和表达式位置

枚举不到目标表

当前数据库是否正确、表名是否猜错、权限是否足够

页面只显示部分结果

是否只取第一行、输出标签是否异常、结果长度是否受限

特别是这两道题,都体现了一个容易忽略的问题:SQL 查询成功,不代表查询结果一定会显示在页面上。

数据库执行逻辑和应用层展示逻辑,需要分别判断。

三、CTF 答题记录模板

每次做 SQL 注入题,都可以按下面的结构记录。这样不容易遗漏步骤,也方便后续整理 WriteUp。

答题检查清单

0/9 已完成

 

1. 信息收集

请求方法、参数名、页面正常响应、后端指纹

2. 注入点确认

特殊字符测试、恒真/恒假对比

3. 闭合方式确认

引号、括号、注释符及 SQL 上下文

4. 列数确认

ORDER BY 探测与 UNION 列数验证

5. 回显位置确认

不同标记值、原查询结果与首行逻辑

6. 数据库信息

数据库版本、当前库、当前连接用户

7. 结构枚举

数据库、数据表、字段名

8. 数据读取

目标字段、返回内容、Flag 格式验证

9. 复现与修复

关键请求、响应证据、根因与修复建议

重置清单复制完整答题步骤

四、最后记住这几个原则

  1. 先判断上下文,再构造语句。 不同题目的引号、括号和参数类型可能不同。

  2. 先确定列数,再寻找回显位。 UNION 查询必须与原查询列数匹配。

  3. 先找库表字段,再读取数据。 不要凭经验假定表名或字段名。

  4. 区分数据库执行和页面显示。 原查询是否返回记录、后端是否只取第一行、哪些字段被展示,都会影响结果。

  5. 把证据写完整。 每一步记录输入、响应和结论,最终 WriteUp 才有复现价值。

从防御角度看,最重要的修复方式是使用 PDO 或 MySQLi 预处理语句,避免把用户输入直接拼接进 SQL;再配合输入白名单、最小数据库权限和关闭生产环境的详细错误输出。仅靠 intval()、隐藏报错或 WAF 都不能代替参数化查询。

posted on 2026-10-19 15:18  GKLBB  阅读(13)  评论(0)    收藏  举报