前言:在SQL注入讲解的前期,我们先使用最经典的sqli-lab靶场进行分析。
一、源码分析:Less-24 的二次注入链路
1. 页面功能与请求链路
- index.php:登录入口,提交到 login.php。
- new_user.php:注册表单,提交到 login_create.php。
- login_create.php:处理注册,写入 users 表。
- login.php:处理登录,设置 $_SESSION["username"] 和 Cookie: Auth=1。
- logged-in.php:登录后页面,显示 $_SESSION["username"],提供修改密码表单。
- pass_change.php:处理改密,二次注入触发点。
- failed.php:失败提示页。
请求链路可以概括为:
注册:new_user.php -> login_create.php -> 写入 users 表
登录:index.php -> login.php -> 查询 users 表 -> 写入 session / cookie
改密:logged-in.php -> pass_change.php -> 从 session 取 username -> 拼接 UPDATE -> 触发二次注入
2. 关键源码解读(因为解读过程同时涉及到一些和Cookie相关的知识,我就顺便简单带过)
(1)login_create.php:存储阶段,埋雷
核心代码:


要点:
- 注册时使用 mysql_escape_string() 对输入转义。
- 转义只保护当前 SQL 语句的语法,不代表数据库里存储的是转义后的内容。
- 例如注册用户名 admin'#:
- 进入 SQL 前会被转义成类似 admin\\'#;
- MySQL 解析字符串时,\\' 表示一个普通单引号;
- 最终写入数据库的值是 admin'#。
- 查重 SQL 查的是 username='admin\\'#',不会和已有用户 admin 冲突,因此可以成功注册。
(2)login.php:登录阶段,取雷
核心代码:


要点:
- 登录处使用 mysql_real_escape_string(),直接闭合注入比较困难。
- 但如果注册了恶意用户名 admin'#,并用它正常登录:
- 输入 admin'# 被转义后,仍能匹配数据库中的 admin'#;
- $row[1] 取出的是数据库里的 username 字段;
- 于是 $_SESSION["username"] 被设置为 admin'#。
- 关键点:session 中的 username 来自数据库,不是用户本次输入,但仍然不可信。
源码这里开始设置cookie:
session_start();
$_SESSION["username"] = $login;
setcookie("Auth", 1, time()+3600);
先用session存放登录用户的会话信息,把用户名传入其中的username字段。再将session_start()生成的session id和setcookie()生成的Auth登录状态放入cookie中发送给浏览器。浏览器收到这个cookie后发送的数据包就会自动带上。程序接收到cookie后通过Auth判断登录状态,通过session id查询session中存放的username信息判断出当前登录的用户。


讲这个是想顺便讲一下这里不安全的cookie机制,这里的源码没做任何校验直接信任了浏览器发出的Auth字段,如果我跳过登录页面直接向改密码页面发送请求,然后在请求中带上伪造的Auth=1的cookie,那么源码就会直接允许我们访问改密码界面(如下图)。虽然说我们没有session id会损失一部分功能,但这样操作能让我们掌握更多和这个网站相关的信息,同时扩大了攻击面。这告诉我们在分析数据包时如果遇到这种不加密的内容,可以尝试简单修改一下发包。特别需要关注带0和1的键值对,因为在网站代码中这常常代表布尔值,可能会影响某个功能的开关。
(3)logged-in.php:展示与改密入口
核心代码:

要点:
- 页面直接显示 $_SESSION["username"](根据返回数据包cookie中的session id判断取出username,下一步取出payload用户名进入UPDATE语句同理)。
- 改密表单提交到 pass_change.php。
- 此时攻击者已经以恶意用户名登录,session 中保存了 admin'#。
(4)pass_change.php:触发阶段,爆炸
核心代码:


要点:
- $username = $_SESSION["username"]; 直接取出,没有再次转义,没有参数化。
- 然后拼接到 UPDATE 语句:
UPDATE users SET PASSWORD='$pass' where username='$username' and password='$curr_pass'
- 如果 $username 是 admin'#,SQL 变成:
UPDATE users SET PASSWORD='hack123' where username='admin'#' and password='任意'
- # 在 MySQL 中注释掉后面内容,实际执行:
UPDATE users SET PASSWORD='hack123' where username='admin'
- 结果:管理员 admin 的密码被修改为 hack123。
3. SQL 模板拆解
| 注册查重 | login_create.php | SELECT count(*) FROM users WHERE username='$username' | POST | 转义保护当前语句 |
| 注册插入 | login_create.php | INSERT INTO users (username,password) VALUES("$username","$pass") | POST | 原始字符可能落库 |
| 登录查询 | login.php | SELECT * FROM users WHERE username='$username' AND password='$password' | POST | 转义保护当前语句 |
| 修改密码 | pass_change.php | UPDATE users SET PASSWORD='$pass' WHERE username='$username' AND password='$curr_pass' | $_SESSION["username"] | 未转义,二次注入触发点 |
-
其中 $username 来自 session,来自数据库,来自第一次注册时的恶意输入。
-
细心的人可能会发现,源码中同时也存在布尔盲注,虽然布尔盲注的原理在文章0x0006讲过了,但这次的场景比之前的所有场景都复杂一点,属于二次注入和布尔盲注结合,所以我简单提一下:
由于在源码文件pass_change.php存在闭合的可能,观察源码或者测试的时候会发现是否对表格内容造成影响最终会导致输出不同(mysql_affected_rows()函数的作用),如果没有对表格内容进行修改,则会执行header('Location: failed.php'),输出一个嘲讽页面,刚好就可以成为布尔盲注的错误标志。布尔注入在这一关并非靶场的本意,网上也很难找到对应的资料,但我觉得这是个可以用于讲解多种手法结合攻击的好例子,大家感兴趣可以按照我下面的方法去尝试。
注册用户admin' AND 0=1#并修改密码:

注册用户admin' AND 1=1#并修改密码:

后续操作只需像文章0x0006讲的布尔盲注一样对and后面的条件内容进行修改再注册用户修改密码,反复测试即可。但需要注意,如果这是一个真实的网站注册入口,在前端或者后端一定是会有长度甚至字符限制的,而且还会结合多因子注册,再加上WAF对关键字进行拦截,想要这样注入几乎是不可能实现。所以讲这个也只是说一个思路,告诉你在真实的网站中我们往往是多种攻击手法结合,而不是像靶场一样死板的单一攻击手法。
4. 二次注入形成条件
二次注入不是一次请求内完成的,它通常满足以下条件:
5. 源码缺陷总结
- 注册时转义 ≠ 永久安全,数据库存储的是原始字符。
- 登录后 session 中保存的 username 来自数据库,但仍然是攻击者可控数据。
- pass_change.php 中 $username 未转义、未参数化,直接拼接 UPDATE。
- 使用 username 作为更新条件,而不是不可篡改的 id。
6. 与 sqli-labs 其他关卡映射
学习文中讲解的cookie原理,分析Less-20源码,借助cookie字段获取所有用户信息。
二、现代网站应用场景
1. 现代开发的变化、ORM / 框架误用
这点和文章0x0002闭合手法的情况类似,不多赘述。
2. 二次注入为什么仍然存在
因为很多系统只保护了第一次写入,却没有保护后续读取再拼接。
风险:
- 写入时参数化,读取后却用 whereRaw、字符串拼接、动态 SQL。
- 数据从数据库、session、缓存、消息队列、日志、第三方接口取出后,再次进入 SQL。
- 多服务架构中,服务 A 写入,服务 B 读取并拼接。
- 代码审计时,重点搜索从 $_SESSION、数据库、缓存、消息队列取出后直接拼接 SQL 的写法。
3. 常见触发点
| 密码修改 / 重置 | 用户名、邮箱 | UPDATE users SET password=? WHERE username='$name' | 字符串拼接 |
| 个人资料更新 | 昵称、邮箱 | UPDATE profile SET … WHERE email='$email' | 拼接 |
| 后台查询 | 订单备注、标签 | SELECT * FROM orders WHERE user='$nickname' | raw SQL |
| 报表 / 导出 | 评论、文章标题 | SELECT … WHERE title LIKE '%$title%' | LIKE 拼接 |
| 动态排序 | 用户偏好 | ORDER BY $column | 动态标识符 |
| 动态表名 | 租户、模块 | SELECT * FROM $table | 动态表名 |
| IN 列表 | 标签、ID 列表 | WHERE id IN ($ids) | 列表拼接 |
| 存储过程 / 动态 SQL | 任意用户数据 | EXECUTE IMMEDIATE | 动态执行 |
4. API / JSON / 微服务场景
二次注入不限于传统表单:
- API 注册接口写入用户名;
- 后台管理服务读取用户名并拼接 SQL;
- 消息队列中的用户数据被消费者拼接进 SQL;
- 缓存中的昵称、标签被取出后进入报表 SQL;
- 多租户 SaaS 后台根据租户名动态查表。
核心仍然是:
数据经过一次存储后,被错误地当成可信数据,再次进入 SQL 结构。
后记:希望各位业内的老师傅能纠正文章的错误
(非商业用途,仅供交流学习,禁止转载)



