当开发者第一次听说"二次注入"这个概念时,往往会感到困惑——明明已经使用了转义函数,为什么还是会出现SQL注入漏洞?这正是Sqli-labs第24关(Less-24)要向我们揭示的核心问题。本文将从一个代码审计师的视角,深入剖析这个经典案例背后的技术细节。
二次注入(Second-Order SQL Injection)是一种特殊的SQL注入形式,它与传统注入的最大区别在于攻击载荷的触发时机。不同于即时执行的注入攻击,二次注入的特点是:
在Less-24的案例中,攻击者可以注册一个精心构造的用户名(如admin'-- ),然后在修改密码功能中触发注入。这会导致系统错误地修改了其他用户(如admin)的密码,而非攻击者自己的账户。
关键点:二次注入的破坏性往往比普通注入更大,因为它能绕过许多常规的输入过滤机制,直接操作数据库中的关键数据。
Less-24的核心漏洞源于对mysql_escape_string()函数的不当使用。让我们通过对比分析来理解这个问题:
| 特性 | mysql_escape_string() | mysql_real_escape_string() |
|---|---|---|
| 连接感知 | 否 | 是 |
| 字符集感知 | 否 | 是 |
| 转义效果 | 固定规则转义 | 根据连接字符集动态转义 |
| 安全性 | 低 | 高 |
| PHP版本 | 5.3.0弃用 | 推荐使用 |
mysql_escape_string()的主要问题在于:
php复制// 危险示例:使用mysql_escape_string()
$username = mysql_escape_string($_POST['username']);
$query = "INSERT INTO users VALUES ('$username', ...)";
// 安全示例:使用mysql_real_escape_string()
$username = mysql_real_escape_string($_POST['username'], $connection);
$query = "INSERT INTO users VALUES ('$username', ...)";
当数据库连接使用多字节字符集时,mysql_escape_string()的缺陷会变得更加明显。考虑以下场景:
0xbf27(在GBK中是一个合法字符)mysql_escape_string()看到的是单引号(0x27)并尝试转义而mysql_real_escape_string()会考虑当前连接的字符集,确保转义后的字符串在特定字符集环境下是安全的。
让我们深入分析Less-24中的完整漏洞链,理解二次注入是如何在代码层面发生的。
在login_create.php中,用户注册处理代码如下:
php复制$username = mysql_escape_string($_POST['username']);
$password = mysql_escape_string($_POST['password']);
$query = "INSERT INTO users (username, password) VALUES ('$username', '$password')";
这里的关键问题是:
mysql_escape_string()真正的漏洞触发点在pass_change.php:
php复制// 直接从SESSION获取用户名,未经任何处理
$username = $_SESSION['username'];
$curr_pass = mysql_real_escape_string($_POST['current_password']);
$pass = mysql_real_escape_string($_POST['password']);
$query = "UPDATE users SET PASSWORD='$pass' WHERE username='$username' AND password='$curr_pass'";
这段代码存在几个严重问题:
mysql_escape_string(),修改密码时使用mysql_real_escape_string()攻击者可以按照以下步骤利用这个漏洞:
admin'-- admin'-- (单引号被转义)sql复制UPDATE users SET PASSWORD='newpass' WHERE username='admin'-- ' AND password='current'
--使条件后半部分失效,实际修改的是admin用户的密码基于Less-24的教训,我们可以总结出以下防御策略:
php复制// 安全的用户名验证示例
if (!preg_match('/^[a-zA-Z0-9_]{3,20}$/', $username)) {
die('Invalid username format');
}
mysql_real_escape_string()或更好的预处理语句php复制// 设置字符集示例
mysql_set_charset('utf8', $connection);
php复制// 使用预处理语句的示例
$stmt = $pdo->prepare("UPDATE users SET PASSWORD=:pass WHERE username=:user");
$stmt->execute([':pass' => $pass, ':user' => $username]);
通过对Less-24的分析,我们可以提炼出几个关键的安全编码原则:
在实际开发中,建议完全避免使用mysql_escape_string()等已弃用的函数,转而使用更现代的数据库访问方式如PDO。同时,代码审计时应特别关注数据流经的各个环节,查找可能存在的信任边界突破点。