警惕PHP中的“魔术哈希”
2015-05-12 18:14:44
阅读:0次
过去十多年来,PHP程序员们一直在跟运算符“==”作斗争。这个运算符引发了许多问题,尤其是对于密码哈希来说更是如此。PHP中的密码哈希是十六进制编码并且可以“0e812389…”形式出现。问题出在“==”跟0e的对比中,这意味着如果以下字符均为数字,那么整个字符串都会被看做浮点。早在五年前Gregor Kopf就指出了这个问题,两年前Tyler Borland与Raz0r也说明了该问题,并且一年前Michal Spacek及Jos Wetzels也有提及,但上周这个问题掀起了更大的波澜。
以下是一个哈希类型类表,当在PHP中使用==运算符后等于0的^0+e\d*$被哈希时会出现。这就意味着当密码哈希以“0e…”开头时,它总是会与如下字符串匹配,而不管当序列的所有字符是“0-9”的数字时中的哪些。也就是说这些数字被哈希时被当成数字“0”处理并且会与其他哈希作对比,对比将会是真值。将“0e…”看做“0是某个值的多少次方”,这样结果总是“0”。PHP将该字符串解释为一个整数。<?php
if (hash('md5','240610708',false) == '0') {
print "Matched.\n";
}
if ('0e462097431906509019562988736854' == '0') {
print "Matched.\n";
}
?>
实际上这就意味着以下“神奇的”字符串被以完全随机的哈希(如随机分配的密码、随机数、文件哈希或凭证)所哈希时,就越可能为真。同样,如果大胆猜想一下,将一个哈希跟相关哈希均以浮点数“0”通过PHP中的“==”运算符比对,并且如果数据库中的另外一个哈希同样以“0e…”开头,结果也将是真。因此,当跟一个哈希数据库对比时,哈希也很有可能是真,即使它们实际上并不匹配。例如许多cookies不过是哈希,而且找到碰撞更多滴依赖于测试时所使用的有效凭证数量。
用例1:使用下列“神奇的”数字当做你想哈希的密码或字符。当跟实际值的哈希对比时,如果它们都被当做“0”因此为真,你就可以在不持有有效密码的情况下登录账户。例如,这种情况的发生场景是:用户在忘记密码流时选择了自动密码,随后立即尝试登录。https://example.com/login.php?user=bob&token=0e462097431906509019562988736854
用例2:攻击者仅需利用下表“哈希(Hash)”一栏中的一个例子将其当做一个值。在某些情况下这些值仅会当做已知值的查询(在内存中,或可能来自数据库并且被比对)。仅仅通过提交哈希值,这个magic hash便会与其他亦被当做“0”的哈希碰撞,因此也会比对为真。
为找到如上结果,我对每个哈希类型都遍历了10亿个哈希整数,试图找到一个当与“0”比对时为真的赋值。如果我在这10亿次尝试中仍未找到,我将会尝试下一个哈希算法。这个方法效率低下但在找到与多数哈希算法有关的“Magic”数字/字符串时,能产生合理效果,另外这些哈希算法的长度为32 hex字符或在单核时较少。其中的一个例外是“adler32”,用于zlib压缩算法并且要求的方法稍有不同。这样做的道理是,对于大多内容来讲,哈希中的熵越多,你的防御就越好。如下是我所使用的代码(adler32要求很多特殊处理以找到不包含特殊字符的有效哈希):<?php
function hex_decode($string) {
for ($i=0; $i
$decoded .= chr(hexdec(substr($string,$i,2)));
$i = (float)($i)+2;
}
return $decoded;
}
foreach (hash_algos() as $v) {
$a = 0;
print "Trying $v\n";
while (true) {
$a++;
if ($a > 1000000000) {
break;
}
if ($v === 'adler32') {
$b = hex_decode($a);
} else {
$b = $a;
}
$r = hash($v, $b, false);
if ($r == '0') {
if(preg_match('/^[\x21-\x7e]*$/', $b)) {
printf("%-12s %s %s\n", $v, $b, $r);
break;
}
}
}
}
?>
我不必使用多数结果中找到的整数,但它让编程序更简单一点。此外,回过头来看,使用整数也更为有效,因为有时候人们强制密码注意字母大小写而数字不受此影响,因此使用整数也更为安全。然而,在实际攻击中,攻击者可能必须找到与密码要求(至少有一个大写字母、一个小写字母、一个数字及一个特殊字符组成)相符的密码,而且再被哈希时会被估值为0。例如,经过1.47亿次暴力尝试之后,我发现md5将“Password147186970!”转换成了“0e153958235710973524115407854157”,满足了严格的密码要求并且估值依然为0。
为完成这个测试,我们发现一个32位字符的哈希在大约1/200,000,000次随机哈希测试中发生碰撞现象。幸亏发生频率不太多,但对于访问量高的网站或会生成许多有效凭证的网站来说,通常已经足够了。不过万幸的是在实际生活中要做到还是很困难的,因为它需要在最可能的实例中发送大规模的尝试。需要注意的是,“0x”(hex)以及“0o”(octal)中也会发生类似问题,但这些字符不会在哈希中出现,因此在多数情况下并不会频繁发生。还有一点需要注意的是,“==” 与 “!=”中也存在类似问题。
网站真的会容易受到此类攻击的困扰吗?答案是肯定的。它在大批不同类型的代码库中会产生问题。在Perl的“==”及“eq”中、以及JavaScript等语言中也会产生相同的困扰。(Jeremi M Gosney对这一问题已做详细解释。)如果出现了与这个问题相关的多个CVE列表,一点都不奇怪。
补丁
很幸运,补丁很简单。如果你用的是PHP,那么你可能听说过人们提到使用三个等号“===”的事儿。这就是原因所在。你所需做的不过是将“==”改为 “===”,并且将 “!=”改为 “!==”来防止PHP猜到变量类型(浮点及字符串)。一些人还建议使用“hash_equals”函数。
WhiteHat将通过自己的动态扫描器及静态代码分析为客户进行测试。点击此处可获得免费检测。使用静态代码分析找到PHP哈希比对非常简单。最后,如果你有一些计算功率且对这个攻击问题感兴趣,可考虑提交任何我们样本中尚未发现、或者我们尚未列出的哈希算法列表的值/哈希对。