恶意数据被“合法保存”的那一刻,漏洞已经注定:二次SQL注入实战解析

admin 2026-01-15 14:49:48 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 文章通过sqli-labs第24关实战解析二次SQL注入。作者分析业务逻辑发现修改密码功能的UPDATE语句存在漏洞。通过注册恶意用户名admin’#,在修改密码时触发注入绕过验证,成功篡改管理员密码。文章强调需关注数据全生命周期安全,避免仅在入口做校验。 综合评分: 93 文章分类: WEB安全,渗透测试,漏洞分析,实战经验,CTF


cover_image

恶意数据被“合法保存”的那一刻,漏洞已经注定:二次 SQL 注入实战解析

原创

武文学网安

武文学网安

2026年1月15日 05:15 西藏

大家好,我是武文。

今天继续挑战学习 sqli-labs

当我来到 第 24 关 时,出现了一个让我非常不安的信号: 所有我熟悉的 SQL 注入手法,全部失效了。

不管是:

  • 在用户名中尝试 admin' or 1=1 #
  • 在登录阶段反复测试各种闭合方式
  • 在修改密码时构造逻辑条件、报错函数

结果都是一样的:

  • 登录有校验
  • payload 被识别
  • 页面没有任何 SQL 报错
  • 看起来一切都“很安全”

一度让我产生了错觉:这一关是不是已经被修好了?

但冷静下来后,我意识到一件事:

👉 这并不符合 sqli-labs 的套路。

既然老办法完全行不通,那问题就一定不在“payload 写得对不对”,而在 思路本身已经需要升级

一、为什么第 24 关的“即时注入”全部失败?

在前面的关卡中,我们习惯的攻击模型是:输入 → SQL 执行 → 页面立即反馈

比如典型的:

SELECT * FROM users WHERE username='$user' AND password='$pass';

只要能控制 $user 或 $pass,就能通过闭合、逻辑判断、报错函数等方式,当场看到结果。

但第 24 关不是这样。

这一关的核心特征是:你输入的数据,并不会立刻参与危险 SQL 执行。

它只是被:

  • 合法校验
  • 正常存储
  • 安静地躺进数据库

危险,并没有消失,只是被延迟了

二、理论篇:什么是二次 SQL 注入(Second Order SQL Injection)?

2.1 一次注入 vs 二次注入

我们熟悉的,大多是一次注入:用户输入 → 拼接 SQL → 立刻执行 → 立刻出问题

而 二次注入 的流程是:

  1. 用户输入恶意数据
  2. 程序 认为它是“安全数据”,存入数据库
  3. 在另一个功能中,把这段数据 重新取出
  4. 未做过滤,直接拼接进 SQL 再次执行
  5. 漏洞,不在输入点,在“第二次使用”时触发

真正的注入点,不在输入时, 而在 “未来某个业务逻辑中”


2.2 为什么开发者很容易踩这个坑?

因为在开发视角里:

  • “这是数据库里的数据”
  • “不是用户刚刚输入的”
  • “之前已经校验过了”

👉 于是默认它是安全的。

但攻击者恰恰就是在**第一次“合法存储”**时埋雷,等着系统在未来某个功能里自己引爆。

三、实操篇:sqli-labs 第 24 关的真实注入思路,从业务流程反推 SQL 执行路径

3.1 先不注入:梳理完整业务流程

访问第 24 关后,可以明确看到这是一个完整用户系统

  • 用户注册(new_user.php)
  • 用户登录(login.php)
  • 用户修改密码(pass_change.php)

这一步先不急着打 payload,而是问一个更重要的问题:每一步,后端“可能”在执行什么 SQL?

3.2 反推三条核心 SQL 语句(关键思维)

结合常见各个页面和 PHP + MySQL 写法,可以合理猜测

注册阶段:

INSERT INTO users (username, password)VALUES ('$username', '$password');

登录阶段:

SELECT * FROM usersWHERE username = '$username'AND password = '$password';

修改密码阶段:

UPDATE usersSET password = '$new_password'WHERE username = '$current_user' and password='$old_password';

到这里,一个非常关键的差异就出现了:

👉 前两条 SQL 使用的是当次用户输入 👉 第三条 SQL 使用的是从数据库中读取出来的数据

这背后,隐藏着一个在真实业务中极其常见、也极其危险的安全假设:

“只要数据已经存进数据库了,它就是安全的。”

而这,正是二次 SQL 注入产生的根源。

3.3 第一反应:能不能在“登录阶段”绕过?

这是一个非常自然、也非常正确的攻击尝试

尝试在登录框中输入:

admin' or 1=1 #admin" or 1=1 #admin') or 1=1 #admin") or 1=1 #

结果发现:

  • 登录失败
  • 页面提示:BUG OFF YOU SILLY DUNMB HACKER

这说明什么?

👉 登录阶段做了输入过滤或预处理 👉 用户输入在“第一次使用”时是安全的

这一步不是失败,是排除法

3.4 第二反应:能不能在“注册阶段”直接显错?

既然前面已经学过在 INSERT 场景下构造 updatexml(),一个很自然的想法是:能不能在注册阶段就触发 error-based?

例如构造这样的SQL语句:

INSERT INTO users (username, password)VALUES (    'admin' AND updatexml(1,concat(0x7e,database(),0x7e),1),    '123456');

在MySQL中,这样的语法是可以反馈当前数据库信息的。

为了闭合可能存在的闭合符号,我们的完整SQL语句大概率是:

INSERT INTO users(username, password)VALUES ('admin' AND updatexml(1,concat(0x7e,database(),0x7e),1) AND '1'='1','123456');

理论上在mysql中同样能够实现XPATH显错注入。

于是尝试在用户名中构造类似payload:

admin' AND updatexml(1,concat(0x7e,database(),0x7e),1) and '1'='1

这一步明显是构造了必然引发数据库错误的语法。但,

实际测试结果是:

  • 注册阶段不会产生任何数据库报错
  • 页面行为始终正常

原因并不复杂:

👉 注册阶段只是把数据原样存入数据库 👉 并不存在「危险 SQL 逻辑」被执行的时机

这一步的意义在于:确认不存在即时注入。

在数据库可以看到我们刚刚注册的数据已经存入进去:

3.5 关键转折:不是“即时注入”,而是“延迟触发”

到这里我才意识到: 我不是没找到注入点,而是一直在错误的时间点寻找注入点。

可以做出一个非常重要的判断:

  • 注册阶段:只是存数据
  • 登录阶段:有过滤
  • 修改密码阶段:使用的是数据库中已有的数据

于是攻击模型发生根本变化:

❌ 不是“我输入了什么,立刻发生什么” ✅ 而是“我现在埋下什么,未来会被怎样使用”

这,正是二次 SQL 注入(Second Order Injection)的典型特征。

3.6 新思路:让“未来会被使用的字段”携带恶意结构

既然真正危险的SQL是:

UPDATE usersSET password = ...WHERE username = '$current_user'AND password = '$old_password';

攻击目标随之明确:

  • 不再想着绕过登录
  • 而是让 用户名本身变成 SQL 结构的一部分

于是 payload 的设计目标变成:当用户名被拼接进 UPDATE 语句时,改变 WHERE 条件的语义

3.7 构造最小、最稳定的 payload

在明确攻击点后,payload 的目标已经非常清晰:

👉 不是绕过登录 👉 而是让用户名在 UPDATE 语句中改变 WHERE 条件的语义

因此这里选择一个“破坏性最小、效果最稳定”的 payload:

admin'#

原因很简单:

  • 不依赖 OR 1=1

  • 不引入额外 SQL 结构

  • 只做一件事:截断 WHERE 条件,可以直接注释掉AND password=’…’这一安全校验

这时我们可以在数据库中看到数据:

damin’# 用户名,密码1234已经被存入数据库中,而原有的admin账户密码为admin。

当用户名被再次拼接进 SQL 时,后端实际执行的是:

UPDATE usersSET password = 'newpass'WHERE username = 'admin'

旧密码校验,在这一刻被彻底绕过。

3.8 利用链闭环:漏洞触发点终于出现

完整利用链如下:

1️⃣ 注册用户名为 admin'# 2️⃣ 使用该用户正常登录 3️⃣ 进入修改密码页面 4️⃣ 提交新密码

后端执行的 SQL 变为:

UPDATE usersSET password = 'newpass'WHERE username = 'admin'#' and password='$old_password';

注意:这里 # 之后的内容虽然在字符串层面看似不完整,但在 MySQL 解析阶段已经被注释,不再参与语法分析。

我们直接实践,将admin’# 密码修改为123

从前端页面可以看到admin’# 的密码成功修改。这接下来让我们来看一下mysql数据库的信息:

可以发现,原有的admin用户密码被修改为123了,而我们注册的admin’# 用户密码仍保持不变。

可以分析知道,在 MySQL 解析阶段:

  • # 之后的内容被直接注释
  • 原本用于校验旧密码的条件被裁剪
  • UPDATE 的作用范围被意外扩大

👉 管理员账号密码被直接覆盖

这样就可以实现修改已经存在的用户的密码,而不需要原账户的密码。这时可以直接用修改后的密码123,直接登录admin用户。

从第 24 关开始,判断 SQL 注入是否存在,已经不能再单纯从“输入框”出发,而必须从数据在系统中是如何被存储、再被使用的这个角度出发。

四、这一关真正危险的地方在哪?

第 24 关的危险性,不在 payload,而在时序

  • 恶意数据先被“合法存储”
  • 漏洞在未来某个功能中触发
  • 安全测试很容易漏掉

这正是为什么二次注入在真实业务中:

  • 更隐蔽
  • 生命周期更长
  • 危害更大

五、结语:第 24 关真正教会我的是什么?

第 24 关并没有教我新的“高级 payload”。

它真正教会我的,是三件事:

  1. 数据库里的数据,也可能是攻击载体
  2. 安全校验只做在“入口”,是远远不够的
  3. 漏洞不一定当场爆炸,而可能“秋后算账”

从这一关开始,我明显感觉到:SQL 注入,已经不再是“技巧问题”,而是

对业务流程和数据生命周期的理解问题


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:武文学网安 武文学网安《恶意数据被“合法保存”的那一刻,漏洞已经注定:二次 SQL 注入实战解析》

评论:0   参与:  0