本文仅用于网络安全技术学习与授权测试交流。本文实验皆在靶场进行,任何未经授权使用文中技术的行为均与作者无关,请务必遵守法律法规,获得许可后方可进行渗透测试。
目录
[1. 正常插入一条数据(作为基准)](#1. 正常插入一条数据(作为基准))
[2. 插入恶意用户(存储 payload)](#2. 插入恶意用户(存储 payload))
[3. 触发二次注入(修改密码)](#3. 触发二次注入(修改密码))
[4. 验证结果](#4. 验证结果)
一、概念
二次注入是一种存储型 SQL 注入漏洞。它与普通(一次)注入的关键区别在于:攻击载荷不是立即触发的,而是先被存储到数据库中,之后在另一个不同的数据库操作中被取出并拼接到 SQL 语句中,从而实现注入。
简单说:攻击者先通过一个入口(如注册)把恶意字符串"寄存"到数据库,再通过另一个入口(如修改资料)激活该恶意字符串,完成攻击。
二、二次注入的原理
二次注入的成功依赖于两个条件:
-
应用程序在第一次存储数据时 使用了安全的 SQL 处理(如参数化查询或转义),使得攻击者的 payload 不会在存储阶段造成危害,而是被原样当作普通字符串存入数据库。
-
应用程序在第二次使用该数据时 ,错误地认为"从数据库中取出的数据是可信的",直接将其拼接到 SQL 语句中,而没有再次进行参数化查询或转义。
步骤详解
第一步:攻击者注入恶意载荷(存储)
-
攻击者在某个输入点(例如注册用户名、发表评论、填写个人签名)提交类似
admin' --的字符串。 -
应用程序在生成插入数据的 SQL 语句时,使用了安全的处理方法。例如:
// 使用参数化查询(安全) $stmt = $conn->prepare("INSERT INTO users (username, password) VALUES (?, ?)"); $stmt->bind_param("ss", $username, $password);或者使用了转义函数:
$username = addslashes($_POST['username']); $sql = "INSERT INTO users (username, password) VALUES ('$username', '$password')";由于转义或参数化,
admin' --中的单引号被转义为\',SQL 语句变为插入admin\' --字符串,实际存储到数据库中的是admin' --(注意:转义只在 SQL 语句中起分隔作用,反斜杠不会存入数据库;或者参数化查询直接存储原始值)。此时数据库中的用户名字段值为
admin' --。
第二步:应用程序在其他功能中使用该数据(触发)
-
攻击者登录(或者该用户名被用于其他业务),应用程序从数据库读取该用户名,例如在"修改密码"功能中:
$username = $_SESSION['username']; // 从数据库或会话中获取,值为 'admin' --' $newpass = $_POST['newpass']; // 危险:直接拼接 $sql = "UPDATE users SET password='$newpass' WHERE username='$username'";由于
$username的值是admin' --,拼接后 SQL 变为:UPDATE users SET password='newpass' WHERE username='admin' -- '在 SQL 中,
--是注释符,后面的内容被忽略,所以实际执行的语句是:UPDATE users SET password='newpass' WHERE username='admin'攻击者成功修改了
admin用户的密码,造成越权。
第三步:注入影响
根据 SQL 语句的不同,攻击可能产生多种后果:绕过登录验证、修改其他用户数据、删除数据、提取敏感信息等。
三、典型应用场景
二次注入常见于以下功能组合:
| 存储点(第一次) | 触发点(第二次) | 示例 payload |
|---|---|---|
| 用户注册 | 修改密码 | 用户名 admin' -- → 修改密码时更新 admin |
| 发表评论 | 后台管理员查看评论 | 评论内容 ' UNION SELECT ... → 管理员查看时执行恶意查询 |
| 个人资料设置 | 登录验证或搜索 | 昵称 test' OR '1'='1 → 登录时可能绕过认证 |
| 上传文件名 | 文件列表显示 | 文件名 test'; DROP TABLE files; -- → 显示时可能执行删除 |
| 搜索记录 | 热门搜索统计 | 搜索词 ' UNION SELECT password FROM users -- → 统计时可能泄露 |
四、与普通注入的对比
| 特性 | 普通(一次)注入 | 二次注入 |
|---|---|---|
| 触发时机 | 同一请求立即执行 | 后续请求中执行 |
| 是否需要数据库存储 | 不需要 | 必须存储到数据库 |
| 攻击载荷形式 | 通常直接提交到注入点 | 先以"无害"形式存储,再被取出 |
| 防护难点 | 输入过滤、参数化查询 | 开发者容易忽略"已存储数据"的风险 |
| 检测难度 | 可通过扫描器发现 | 需要模拟完整业务流程,较隐蔽 |
五、二次注入的根本原因
开发人员存在错误的安全假设:
-
"我们已经对输入数据进行了转义或参数化,所以存储到数据库的数据是安全的。"
-
"从数据库取出的数据是可信的,不需要再过滤。"
这种假设忽略了数据可能包含恶意 SQL 语法元素(如单引号、注释符),当这些元素被拼接到新的 SQL 语句时,就会重新激活。
六、防御措施
-
始终使用参数化查询(预编译语句) 无论数据来自用户输入、数据库、文件还是任何其他源,只要拼接到 SQL 语句中,就必须使用参数化。这是根本性解决方案,与数据来源无关。
$stmt = $conn->prepare("UPDATE users SET password=? WHERE username=?"); $stmt->bind_param("ss", $newpass, $username); -
对存储的数据也进行转义(若无法使用参数化) 若因遗留代码无法立即整改,在拼接前对从数据库取出的数据使用数据库特定的转义函数(如
mysqli_real_escape_string),但这不如参数化可靠。 -
最小权限原则 数据库账户只授予必要的权限。例如,用于 Web 应用的账户不应有
DROP、TRUNCATE权限;对于UPDATE操作,可限制其只能更新特定字段。 -
输入验证 在存储前,对输入数据做严格的类型校验(如数字型用
intval,枚举值用白名单)。例如,用户名可以限制只允许字母数字和下划线,这样即使存在二次注入点也无法构造 payload。 -
代码审计 重点检查"数据先存储后使用"的业务逻辑,例如:注册→登录、评论→审核、上传→下载等。查找从数据库读取数据后直接拼接到 SQL 的代码模式。
-
使用 ORM 框架 现代 ORM(如 Laravel Eloquent、Hibernate、Entity Framework)默认使用参数化查询,能大幅降低二次注入风险。
七、检测方法
-
黑盒测试:
-
在可能存储的位置(注册、评论等)输入 payload,如
test' OR '1'='1、admin' --、' UNION SELECT 1,2,3 --。 -
然后访问可能使用这些数据的页面(修改资料、查看评论、重置密码等),观察是否出现异常(如登录了其他账户、页面显示额外数据、报错信息)。
-
比较响应与正常情况,判断是否存在注入。
-
-
白盒测试:
-
搜索代码中从数据库查询结果的赋值语句(如
$row['username'])。 -
追踪该变量是否在后续被用于拼接 SQL 语句(如
"SELECT ... WHERE name = '$name'")。 -
若拼接处未使用参数化查询,且
$name来源于数据库,则可能存在二次注入。
-
八、真实案例
-
邮箱激活 :用户注册时输入
user@example.com' OR '1'='1,应用程序安全存储。激活邮件链接中使用了该邮箱作为参数并拼接到 SQL 查询用户信息,导致返回所有用户数据。 -
商品评价 :评价内容写入
test', 'popularity'=100 --,管理员在后台统计评分时未过滤,导致所有商品的 popularity 被修改。
九、靶场注入示例
一、环境与代码
demo6.php(用户注册/插入)
$username = addslashes($_GET['username']); $password = addslashes($_GET['password']); $sql = "insert into userinfo(username,password) values('{$username}','{$password}')";

-
使用
addslashes转义单引号等特殊字符。 -
转义只在 SQL 语句中生效,实际存入数据库的是原始字符串(例如
admin'-- -中的单引号不会被转义存储,但 SQL 语句中变成admin\'-- -,数据库存储的是admin'-- -)。
demo7.php(修改密码)
$username = $_GET['username']; // 直接获取,未过滤 $password = $_GET['password']; $sql = "update userinfo set password='{$password}' where username='{$username}'";

- 直接拼接用户输入,存在 SQL 注入漏洞。
二、攻击步骤
1. 正常插入一条数据(作为基准)
http://127.0.0.1/demo6.php?username=admin&password=123456

insert into userinfo(username,password) values('admin','123456')
数据库中增加用户 admin,密码 123456。

2. 插入恶意用户(存储 payload)
Payload:
http://127.0.0.1/demo6.php?username=admin'-- -&password=123456

代码执行流程:
-
addslashes将用户名中的单引号'转义为\',即admin\'-- -。 -
拼接后的 SQL 语句变为:
insert into userinfo(username,password) values('admin\'-- -','123456') -
此时字符串中的
'admin\'-- -'被当作一个完整的字符串值,其中的--不再是 SQL 注释符 ,因为它被包含在单引号内。因此该INSERT语句正常执行,成功向数据库插入了一条记录,用户名字段值为admin'-- -(反斜杠仅用于 SQL 解析,实际入库的是admin'-- -)。
数据库中存储的结果:
| id | username | password |
|---|---|---|
| 15 | admin'-- - | 123456 |

3. 触发二次注入(修改密码)
攻击者访问 demo7.php,传入恶意用户名和密码:
http://127.0.0.1/demo7.php?username=admin'-- -&password=111111

SQL 拼接:
update userinfo set password='111111' where username='admin'-- -'
由于 -- 注释了后面的内容,实际执行:
update userinfo set password='111111' where username='admin'

成功将 admin 用户的密码修改为 111111,实现了越权。
4. 验证结果
数据库中的 admin 密码被改为 111111
三、漏洞原理
-
第一次插入 :虽然使用了
addslashes转义,但数据库中存储的用户名是admin' --(反斜杠仅用于 SQL 语句解析,不进入存储)。 -
第二次更新 :
demo7.php直接拼接用户名,没有进行任何过滤或参数化,导致存储的 payload 中的单引号和注释符生效,改变了 SQL 语义,将原本修改"admin' --"的语句变成了修改"admin"。
四、总结
二次注入的核心是:数据在第一次被安全存储后,第二次使用时因缺乏过滤而触发注入 。本实验中,demo6.php 安全存储了恶意用户名,demo7.php 的不安全拼接导致了越权修改。
十、VAuditDemo靶场注入再演示
一、靶场简介
-
名称:VAuditDemo(一个用于演示常见 Web 漏洞的 PHP 程序)
-
漏洞点:用户注册 + 修改密码功能
-
漏洞类型:二次注入(Second-Order SQL Injection)
-
核心原因:注册时对特殊字符进行了转义安全存储,但修改密码时直接拼接数据库取出的用户名,导致 SQL 语义改变。
二、注入过程
第一步:
先注册lisi'#账号,密码123456

第二步:
注册完成点击退出,再注册

第三步:
注册个lisi账号,密码123456

第四步:
可以看到正常登录lisi账号

第五步:
可以看到正常登录lisi'#账号

第六步:
更改lisi'#账号的密码,更改为111111

第七步:
发现lisi'#用修改完的密码登录不进去

第八步:
但是lisi账号能用密码111111登录进去

三、漏洞原理深入分析
| 阶段 | 操作 | SQL 上下文 | 是否安全 | 原因 |
|---|---|---|---|---|
| 注册 | 插入 lisi'# |
INSERT 语句,使用了 addslashes 转义 |
安全 | 单引号被转义,# 作为普通字符串 |
| 登录 | 查询 lisi'# |
可能使用参数化查询或转义 | 安全 | 能正确匹配记录,无注入 |
| 修改密码 | 拼接 lisi'# 到 UPDATE |
直接拼接,未过滤 | 不安全 | # 成为注释符,导致修改了 lisi 的密码 |
根本原因 :开发人员错误地认为"从数据库取出的数据是可信的",因此没有对 $username 进行任何安全处理。然而,数据库中存储的原始字符串可能包含 SQL 元字符(如 '、#、--),当这些字符被拼接到新的 SQL 语句中时,就会改变语义。
四、与普通注入的区别
-
普通注入:攻击数据直接从用户输入进入 SQL 语句,在同一请求中生效。
-
二次注入:攻击数据先被存储(通常经过转义),然后在另一个请求中被取出并拼接到 SQL 中,由于第二次缺乏防护而触发。
五、防御措施
-
参数化查询(Prepared Statements) 无论是 INSERT、SELECT 还是 UPDATE,一律使用预编译语句,彻底隔离代码和数据。
$stmt = $conn->prepare("UPDATE users SET password=? WHERE username=?"); $stmt->bind_param("ss", $newpass, $username); -
输入过滤与输出转义结合 即使从数据库取出数据,也要根据其用途(如拼接到 SQL)进行转义或类型转换。
-
最小权限原则 数据库账户仅授予必要的权限(例如,Web 应用账户不应有
DROP、TRUNCATE等权限)。 -
禁用危险字符 对用户名等字段限制可接受的字符集(如字母数字 + 下划线),禁止
'、#、-等特殊字符。
六、总结
VAuditDemo 的二次注入漏洞展示了典型的数据流攻击:攻击者通过注册功能"预埋"一个包含注释符的恶意用户名,然后在修改密码功能中利用该用户名拼接 SQL,实现越权修改其他用户的密码。这提醒开发者:永远不要信任任何来源的数据,包括自己数据库中的内容;所有拼接 SQL 的操作都必须经过参数化查询或严格转义。