免责声明本文全部实验操作均在本人完全可控的自建本地靶场环境完成,文章仅用于网络安全原理学习与技术研究。根据《中华人民共和国网络安全法》,未经授权对任何第三方系统进行探测、命令执行、文件写入等操作属于违法行为。
严禁复制、使用本文中的 Payload、代码应用到未获得授权的设备上。若读者将文中内容用于非法用途,一切法律责任由行为人本人承担,与本文作者无关。
个人认知有限,文中难免存在疏漏与冗余,如有不妥之处,欢迎大家交流指正,还望海涵。
————————————————
SQL 二次注入(Second-Order SQL Injection)
一、什么是二次注入
二次注入,又称二阶 SQL 注入(Second-Order SQL Injection),是一种特殊的 SQL 注入形式。它和传统的(一阶)SQL 注入最大的区别在于:
一阶注入:攻击者提交的恶意数据,在当前请求中就被直接拼接到 SQL 语句并执行,攻击效果立即显现。
二次注入:攻击者提交的恶意数据,在写入时被正确地转义、过滤或使用参数化处理,安全地存储进数据库;但当这些数据在后续的某个请求中被读取出来,并再次被拼接进新的 SQL 语句执行时,由于未做二次过滤,注入漏洞才真正被触发。
通俗地说:"第一次存储是安全的,第二次使用时是危险的。"
二次注入的关键在于,攻击载荷经过了"存储 → 读取 → 拼接执行"这一完整链条,攻击效果具有延迟性和隐蔽性,因而往往能绕过仅针对"输入"进行过滤的常规防御。
二、产生原理
二次注入产生的根本原因可以概括为一句话:
应用程序对写入数据库的数据做了过滤/转义,但对从数据库读取出来的数据却盲目信任,直接拼接到 SQL 语句中。
其完整流程如下:
为什么常见防御会失效?
以"对单引号进行转义"为例(如使用addslashes()或反斜杠转义):
攻击者提交用户名
admin' --应用层使用
addslashes()转义后变为admin\' --,此时拼接进 INSERT 语句是安全的。但数据库实际存储的字符串是
admin' --(转义字符只在拼接 SQL 时起作用,并不写入库)。后续某功能(如修改密码、查询资料)执行:
UPDATE users SET password='新密码' WHERE username='admin' --'此时从库里读出的
admin' --被原样拼接,'闭合了前面的引号,--注释掉了后续内容,注入由此触发。
关键认知:转义/过滤只能保证"这一次"拼接安全,并不能改变数据本身的"危险内容"。一旦这些内容被再次使用,就必须重新处理。
三、利用条件
要成功实施二次注入,通常需要满足以下条件:
存在"先存储后使用"的数据流:某功能将用户可控的数据写入数据库,另一功能又从数据库读取该数据。
写入时经过处理(可选但常见):写入端可能做了转义/过滤/参数化,导致一阶注入不可行,攻击者被迫走二次注入路径。
读取后的数据被拼接进 SQL:后续使用该数据时,没有使用参数化查询或再次过滤,而是直接拼接字符串。
攻击者可构造并观察到差异:通常需要借助布尔盲注、时间盲注或回显差异来判断注入结果。
四、利用过程示例
4.1 场景假设
一个网站有两个功能:
注册功能:用户填写用户名、密码,注册账号(写入数据库)。
修改密码功能:登录后输入新密码,系统根据当前登录用户名更新密码(读取用户名并拼接 SQL)。
4.2 攻击步骤
第一步:注册一个恶意用户名
攻击者注册用户名:admin' #
(假设注册时后端使用了转义处理,该用户名被安全地写入数据库,注册成功。)
数据库users表此时存储:
| id | username | password |
|---|---|---|
| 1 | admin' # | 123456 |
第二步:触发二次注入
攻击者以admin' #身份登录,进入修改密码功能,提交新密码hacked。
后端修改密码的核心 SQL 大致为:
UPDATE users SET password = 'hacked' WHERE username = '$username';其中$username是从数据库读出的当前登录用户名,即admin' #,拼接后变成:
UPDATE users SET password = 'hacked' WHERE username = 'admin' #';由于#(或--)将其后内容注释,实际执行的语句等价于:
UPDATE users SET password = 'hacked' WHERE username = 'admin';结果:攻击者成功将真正管理员账号admin的密码修改为hacked,从而接管管理员账号。
4.3 常见攻击目标
垂直越权:普通用户通过注入影响高权限用户的数据(如上例)。
获取敏感数据:借助布尔盲注/时间盲注逐字符读取其他表数据。
删除/篡改数据:构造
DELETE、UPDATE等破坏性载荷。
五、常见应用场景
二次注入多发于以下功能点:
注册/登录 + 个人资料修改:恶意用户名在修改资料、改密时被再次拼接。
评论/留言 + 后台管理:评论内容被存储后,在后台的搜索、删除、筛选等 SQL 中被再次使用。
收货地址/联系人信息:存储的地址、姓名在订单查询、统计功能中被拼接。
用户昵称 + 论坛引用/回复:昵称在展示或引用他人帖子时被再次拼入查询。
导入的数据文件:批量导入的数据(CSV 等)在后续报表、检索中被拼接。
配置项/自定义字段:用户可配置的字段值在系统内部 SQL 中复用。
六、练习
一道恒学练习的ctf题目
在靶场发表一篇文章
总共有5个字段
拼接语句,再次查看文章
注入语句在页面回显
尝试查看当前数据库
成功得到当前数据库
获取数据库列名
1',2,(select group_concat(table_name) from information_schema.tables where table_schema='php_test'),4,NOW())--
获取数据库字段名
1',2,(select group_concat(column_name) from information_schema.columns where table_schema='php_test' and table_name='articles'),4,NOW())--
获取flag
1',2,(select group_concat(password) from php_test.users),4,NOW())--
sqli第24关
注册账户:admin'#
使用注册的账户登录,登录后可以修改密码
修改密码的逻辑:
UPDATE users SET password='新密码' WHERE username='admin'#' AND password='旧密码'#注释后面的信息,从而变成修改管理员的账户admin密码为1234
最终生效语句:
UPDATE users SET password='新密码' WHERE username='admin'使用修改后的密码成功登录admin账户
七、防御措施
二次注入的防御核心是:不信任任何进入 SQL 的数据,无论它来自用户输入还是数据库读取。
7.1 使用参数化查询(预编译语句)——最根本手段
无论数据来自哪里,一律使用占位符绑定参数,从根本上杜绝拼接:
// Java JDBC PreparedStatement String sql = "UPDATE users SET password = ? WHERE username = ?"; PreparedStatement ps = conn.prepareStatement(sql); ps.setString(1, newPassword); ps.setString(2, username); ps.executeUpdate();
# Python cursor.execute( "UPDATE users SET password = %s WHERE username = %s", (new_password, username) )
参数化查询将"数据"与"代码"分离,任何来自数据库的字符串都只被当作数据,不可能被解析为 SQL 语法。
7.2 使用 ORM 框架
主流 ORM(如 Hibernate、MyBatis 的#{}、SQLAlchemy、Eloquent 等)默认对参数进行绑定。注意:
MyBatis 中务必使用
#{}而非${},${}是字符串拼接,仍存在注入风险。
7.3 对"输出侧"同样进行过滤/转义
若因历史原因必须拼接 SQL,那么在从数据库读出数据并再次使用时,同样执行与输入侧一致的过滤、转义,而不是默认"库里的数据是干净的"。
7.4 最小权限原则
为应用配置最小化的数据库账号权限,限制 SQL 语句的破坏能力:
不同功能使用不同权限的数据库账号。
严格限制
DELETE、UPDATE、DROP等高危操作权限。
7.5 白名单校验与输入约束
对用户名、昵称等字段在写入时就限制字符集,例如只允许字母、数字、下划线,直接拒绝含特殊字符(
'、"、#、--、;等)的输入。从源头减少危险数据入库的可能性。
7.6 使用 Web 应用防火墙(WAF)与安全审计
部署 WAF 可在一定程度上检测并拦截明显注入载荷。
配合代码审计工具、静态扫描工具在开发阶段发现"数据二次使用但未过滤"的隐患点。
7.7 其他辅助措施
安全编码培训与规范:建立统一的数据库访问层,强制使用参数化接口,禁止业务代码拼接 SQL。
代码评审:重点审查"从数据库读取 → 拼接 SQL"的链路。
定期渗透测试:二次注入隐蔽性强,需通过专业测试发现。
八、总结
| 维度 | 一阶注入 | 二次注入 |
|---|---|---|
| 触发时机 | 当前请求立即触发 | 后续请求延迟触发 |
| 载荷存放 | 直接进入 SQL | 先入库,再取出进入 SQL |
| 写入侧处理 | 通常未过滤 | 通常已过滤/转义 |
| 防御盲点 | 输入过滤缺失 | 对"库内数据"盲目信任 |
| 检测难度 | 较低 | 较高(隐蔽、延迟) |
核心结论:
二次注入的根源是对"数据库中读出的数据"盲目信任,而本质上它仍是 SQL 注入。
防御的关键不是"在输入时过滤一次",而是彻底使用参数化查询,让所有数据——无论来源——都无法被当作 SQL 代码执行。
只要坚持"数据与代码分离"这一原则,无论一阶还是二阶注入都能被有效防范。