1.原理知识:
二阶SQL注入攻击(Second-Order SQL Injection)原理详解
一、基本概念
二阶注入是一种"存储型"SQL注入,攻击流程分为两个阶段:
- 首次输入:攻击者将恶意SQL片段存入数据库
- 二次调用:当应用程序使用存储的数据构造SQL查询时触发攻击
二、典型攻击场景(以密码更新为例)
三、具体攻击步骤分解
初始注入阶段
-- 攻击者注册特殊用户名 INSERT INTO users(username) VALUES ('admin''-- ')
存储结果:
id username 1 admin'-- 触发阶段(密码更新操作)
// 应用代码示例(存在漏洞) $newPassword = $_POST['password']; $username = $_SESSION['username']; // 从会话获取"admin'--" $sql = "UPDATE users SET password='$newPassword' WHERE username='$username'";
实际执行SQL:
UPDATE users SET password='hacked' WHERE username='admin'-- '
效果:
--
注释掉了后续条件- 所有用户的密码被更新为
hacked
四、与普通注入的关键区别
特性 | 普通SQL注入 | 二阶注入 |
---|---|---|
触发时机 | 即时执行 | 延迟执行(数据被使用时) |
输入点 | 直接拼接的输入参数 | 存储在数据库中的数据 |
防御难度 | 较易检测 | 更难发现 |
五、防御方案
1. 数据存储阶段
// 存入前转义+类型检查
$username = $pdo->quote($_POST['username']);
// 或使用预处理
$stmt = $pdo->prepare("INSERT INTO users(username) VALUES (?)");
$stmt->execute([$_POST['username']]);
2. 数据使用阶段
// 永远使用参数化查询(即使数据来自数据库)
$stmt = $pdo->prepare("UPDATE users SET password=? WHERE username=?");
$stmt->execute([$newPassword, $dbStoredUsername]);
3. 深度防御措施
- 数据净化:在读取数据库后仍进行过滤
function sanitizeDbValue($value) { return preg_replace('/[^\w@.-]/', '', $value); }
- 权限分离:
CREATE USER 'app_update'@'localhost' IDENTIFIED BY 'pass'; GRANT UPDATE(password) ON db.users TO 'app_update'@'localhost'; -- 无权限修改其他字段
4. 安全审计建议
-- 监控非常规模式
CREATE TRIGGER audit_password_changes
AFTER UPDATE ON users
FOR EACH ROW
INSERT INTO audit_log VALUES(NEW.id, '密码变更', NOW());
六、实际案例检测
检测代码是否脆弱:
// 危险代码模式(可被二阶注入利用)
function updatePassword($conn, $newPass){
$user = getUserFromDB(); // 从数据库获取用户名
$conn->query("UPDATE users SET pass='$newPass'
WHERE user='{$user['name']}'");
}
// 安全版本
function safeUpdatePassword($pdo, $newPass){
$user = getUserFromDB();
$stmt = $pdo->prepare("UPDATE users SET pass=? WHERE user=?");
$stmt->execute([$newPass, $user['name']]);
}
七、进阶防护:ORM层防护
// 使用Laravel Eloquent示例
User::where('name', $request->username)
->update(['password' => bcrypt($request->password)]);
// 自动参数化,无需手动处理
关键原则:所有来自外部和内部的数据在拼接SQL时都应视为不可信的,必须通过参数化查询处理。
2.更新密码的二阶注入攻击关系解释
用户注册的特殊账号会再更新密码的过程中引起二阶注入攻击。
更新密码场景下的二阶SQL注入攻击原理详解
一、攻击流程全貌(以密码更新为例)
二、关键漏洞形成原理
第一次存储时的"无害"数据
-- 注册时(使用了参数化查询,此时安全) INSERT INTO users(username) VALUES('admin''--')
数据库实际存储:
id username 1 admin'-- 密码更新时的危险拼接
// 漏洞代码示例 $username = getUsernameFromDB(); // 获取存储的"admin'--" $newPass = $_POST['password']; // 直接拼接SQL(未使用参数化) $sql = "UPDATE users SET password='$newPass' WHERE username='$username'";
生成的恶意SQL:
UPDATE users SET password='hacked' WHERE username='admin'-- '
注入效果
--
注释掉了原SQL的条件部分- 语句等效于:
UPDATE users SET password='hacked'
- 所有用户的密码被修改
三、与传统注入的核心区别
特性 | 传统注入 | 二阶注入 |
---|---|---|
输入点 | 直接来自HTTP请求 | 来自数据库存储的数据 |
触发条件 | 立即执行 | 需要特定业务逻辑触发 |
防御盲区 | 输入过滤即可防御 | 需要全程参数化 |
检测难度 | 容易被扫描器发现 | 需要业务流分析 |
四、完整攻击实例分析
攻击准备阶段
POST /register HTTP/1.1 username=admin'--&password=123
- 后端安全地存储了该用户名(此时无风险)
触发攻击阶段
POST /update-password HTTP/1.1 new_password=hacked
- 后端操作流程:
// 从数据库获取当前用户名 $user = $db->query("SELECT username FROM users WHERE id = {$_SESSION['id']}")->fetch(); // 危险操作:拼接SQL $db->exec("UPDATE users SET password='hacked' WHERE username='{$user['username']}'");
- 后端操作流程:
实际执行的恶意SQL
UPDATE users SET password='hacked' WHERE username='admin'-- '
五、针对性防御方案
1. 存储阶段防御
// 注册时严格过滤
function validateUsername($name) {
return preg_match('/^[a-z0-9_]{3,20}$/i', $name);
}
// 使用预处理语句存储
$stmt = $pdo->prepare("INSERT INTO users(username) VALUES(?)");
$stmt->execute([$_POST['username']]);
2. 使用阶段防御
// 密码更新操作(安全版本)
function updatePassword($userId, $newPass) {
$pdo = new PDO(...);
// 步骤1:获取用户名(使用参数化)
$stmt = $pdo->prepare("SELECT username FROM users WHERE id = ?");
$stmt->execute([$userId]);
$user = $stmt->fetch();
// 步骤2:更新密码(再次参数化)
$stmt = $pdo->prepare("UPDATE users SET password = ? WHERE username = ?");
$stmt->execute([password_hash($newPass, PASSWORD_BCRYPT), $user['username']]);
}
3. 深度防御措施
数据库权限隔离
CREATE USER 'app_user'@'localhost' IDENTIFIED BY 'pass'; GRANT SELECT, UPDATE(password) ON db.users TO 'app_user'@'localhost'; -- 禁止修改username等其他字段
审计日志
CREATE TRIGGER password_change_audit AFTER UPDATE ON users FOR EACH ROW INSERT INTO security_log VALUES( USER(), CONCAT('Password changed for ', OLD.username), NOW() );
六、漏洞检测方法
代码审计关键点
- 查找所有从数据库读取数据后直接拼接SQL的地方
- 特别关注:用户资料更新、密码重置、权限变更等敏感操作
自动化测试脚本
# 二阶注入测试脚本示例
def test_second_order_injection():
# 注册测试账号
register("test'--", "123456")
# 修改密码
resp = update_password("new_password")
# 验证是否所有用户密码被修改
admin_pass = get_password("admin")
assert admin_pass == "new_password" # 如果成立说明漏洞存在
七、现代框架的最佳实践
// Laravel示例(自动防护二阶注入)
User::where('username', $request->username)
->update(['password' => bcrypt($request->new_password)]);
// Eloquent ORM自动使用参数化查询
// 即使这样也是安全的:
$user = User::find($id);
DB::update("UPDATE users SET password = ? WHERE username = ?",
[$newPass, $user->username]);
关键安全原则:无论数据来自HTTP请求还是数据库,在拼接SQL时永远使用参数化查询。这种防御方式能同时防护一阶和二阶SQL注入攻击。
3.建立具有密码更新功能的网站
前端html页面+后端php页面
3.1用户注册页面(html)
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Register</title>
<style>
#a{ width: 300px; text-align: right; }
.b{width: 150px;height:20px;}
</style>
</head>
<body>
<div id=a>
<form name="form_register" method="post" action="check_register.php">
Username: <input type="text" class=b name="username" /> <br>
Psssword: <input type="password" class=b name="passwd" /> <br>
<input type="submit" name="Submit" value="Submit" />
<input type="reset" name="Reset" value="Reset" />
</form>
</div>
</body>
</html>
3.1实现注册功能(php)
<?php
//包含数据库连接
include('con_database.php');
//获取输入的信息
$username = isset($_POST['username']) ? mysqli_escape_string($con,$_POST['username']) : '';
$passwd = isset($_POST['passwd']) ? mysqli_escape_string($con, $_POST['passwd']) : '';
if($username == '' || $passwd == '' )
{
echo "<script>alert('信息不完整!'); history.go(-1);</script>";
exit;
}
//执行数据库查询,判断用户是否已经存在
$sql="select * from users where username = '$username' ";
$query = mysqli_query($con,$sql)
or die('SQL语句执行失败, : '.mysqli_error($con));
$num = mysqli_fetch_array($query); //统计执行结果影响的行数
if($num) //如果已经存在该用户
{
echo "<script>alert('用户名已存在!'); history.go(-1);</script>";
exit;
}
$sql = "insert into users (username,passcode) values('$username','$passwd')";
mysqli_query($con, $sql)
or die('注册失败, : '.mysqli_error($con));
echo "注册成功,请<a href='login.html'>登录</a>";
mysqli_close($con);
?>
3.3登录验证(php)
<?php
//包含数据库连接
include('con_database.php');
//获取输入的信息
$username = isset($_POST['username']) ? mysqli_escape_string($con,$_POST['username']) : '';
$passwd = isset($_POST['passwd']) ? mysqli_escape_string($con, $_POST['passwd']) : '';
if($username == '' || $passwd == '' )
{
echo "<script>alert('请输入用户名和密码!'); history.go(-1);</script>";
exit;
}
//从数据库查询
$sql = "select * from users where username = '$username' and passcode = '$passwd' ";
$res = mysqli_query($con,$sql) or die('SQL语句执行失败, : '.mysqli_error($con));
$row = mysqli_fetch_row($res);
if ($row[0])
{
session_start();
$_SESSION['username'] = $row[1];
echo $row[1].'欢迎访问!';
echo "<br>";
echo "<a href='updatepasswd.html'>修改密码</a>";
}
else
{
echo "<script>alert('用户名或密码错误!'); history.go(-1);</script>";
}
mysqli_close($con);
?>
3.4更新密码 (html)
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>UpdatePassword</title>
<style>
#a{ width: 300px; text-align: right; }
.b{width: 150px;height:20px;}
</style>
</head>
<body>
<div id=a>
<form name="form_register" method="post" action="updatepasswd_mysqli.php">
当前密码: <input type="text" class=b name="current_passwd" /> <br>
新密码: <input type="password" class=b name="passwd" /> <br>
<input type="submit" name="Submit" value="Submit" />
<input type="reset" name="Reset" value="Reset" />
</form>
</div>
</body>
</html>
3.5更新密码功能(php)
<?php
session_start();
if(!isset($_SESSION['username']))
{
//重新定位到注册页面
header('Location: register.html');
}
if (isset($_POST['Submit'])){
//包含数据库连接
include("con_database.php");
// $username = $_SESSION['username'];
$username = mysqli_real_escape_string($con,$_SESSION['username']);
$curr_pass= mysqli_real_escape_string($con,$_POST['current_passwd']);
$pass= mysqli_real_escape_string($con,$_POST['passwd']);
$sql = "UPDATE users SET passcode = '$pass' WHERE username = '$username' and passcode = '$curr_pass' ";
$res = mysqli_query($con,$sql) or die('SQL执行失败 :'.mysqli_error($con));
$row = mysqli_affected_rows($con);
if($row != 0)
{
echo "<script>alert('密码更改成功!'); history.go(-1);</script>";
}
else
{
echo "<script>alert('当前密码错误!'); history.go(-1);</script>";
}
}
mysqli_close($con);
?>
4.用户使用功能测试
检查apache打开
报错,原因是我是双击打开的html,应该用http协议打开
注册之后需要用到登录页面
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Login</title>
<style>
#a{ width: 300px; text-align: right;}
.b{width: 150px;height:20px;}
</style>
</head>
<body>
<div id=a>
<form name="form_login" method="post" action="check_login.php">
Username: <input type="text" class=b name="username" /> <br>
Psssword: <input type="password" class=b name="passwd" /> <br>
<input type="submit" name="Submit" value="Submit" />
<input type="reset" name="Reset" value="Reset" />
</form>
</div>
</body>
</html>
修改密码,用到updatepasword