本文翻译自:PHP Challenge 2015 Solution
1. 题目
本题目源于PHP5.6的一个更新,目标是以admin身份登录。
1 | <?php |
2. MD5 密码Hash
当浏览代码时,第一眼看到应该是下面的用户表。
1 | <?php |
这段代码显示了11名用户,从0到10。还可以看到,在用户后面有32字符的字符串,通过下面的md5()
可以猜测这是密码的MD5值。使用MD5加密密码是一个很糟糕的主意,因为像彩虹表这种先进的破解技术可以破解一些简单的密码(较短的或简单的密码)。然而,我们并不希望大家暴力破解MD5来解出此题。
我们选择了一些无法Google出结果的密码,除了用户5的密码Hash06e2b745f3124f7d670f78eabaa94809
,密码明文是hund
,在德语中是狗的意思。但是,本题的目标是以admin的身份登录,以用户5的身份登录只是一个解出题目的第一步。
我们为其他用户选择了相当长的强密码,我们并不想让任何人破解出密码明文。甚至一些密码是随机产生的,就连我们自己都不知道明文是什么。
但是,admin的密码明文是orewgfpeowöfgphewoöfeiuwgöpuerhjwfiuvugeröwhdböcp9ueiwrbcvzeiwuböochineworöcern
,想在密码字典中找到这样的密码基本不可能。
所以,需要寻找其他的解法。
3. 理解代码
1. 定义用户
1 | <?php |
2. 读取用户Cookie
这段代码从COOKIE中读取user
信息,并在为检查其到底是不是数组的情况下,就将其看成数组。数组中下标为1的元素是密码的明文,然后对其进行MD5计算。
1 | <?php |
3. 在用户表中对照密码
这段代码遍历用户表,检查登录密码和用户表中密码是否一致。将用户ID和密码Hash分离放在数组中,然后使用三等于===
运算符和登录数组进行比较。使用三等于运算符来进行判断是正确的做法,因为在PHP中,所有和安全相关的比较都应该使用三等于运算符。为了后面比较$uid
,这里将cookie中的$uid
加上0来强制转换为数字类型。此外,还将$valid_user
设为true
。
1 | <?php |
如果你仔细阅读上面的代码,你会发现在匹配到已存在的用户后,循环依然继续,没有直接退出循环。这确实是个bug,但在这种特定情况下,并不能成为一个可利用的缺陷。
4. 处理用户登录
如果没有检测到有效用户,在比较循环结束后,脚本立即退出,并报错。如果是有效用户,这里有两种情况:$uid
为0的admin用户,和其他普通用户。使用双等于而不是三等于来和0进行比较,由此判断是否为admin用户。使用双等于来进行安全相关的比较是一种糟糕的方式。但是,这里的错误使用和解题并没有关系。
1 | <?php |
4. 我们现在知道什么?
我们现在知道从COOKIE中获取数据,作为数组进行储存。数组包含两个元素:用户ID和密码明文。还知道用户5的密码明文是hund
。这意味着,我们可以通过设置cookie以用户5的身份进行登录。
1 | Cookie:user[0]=5;user[1]=hund; |
同时,我们知道前面使用三等于比较用户,后面用双等于比较用户ID。
到这里时,很多人会卡住,因为他们不知道PHP5.6修复了什么bug。但是有20-30人解出了这道题,因为他们从PHP的Changelog里看到了
Changelog
PHP每发行一个新版本,都会发布Changelog和新闻文件,有的时候,Changelog里会有写有趣的信息,里面会列出修复的问题,并且后面会有提交bug的链接,里面会有测试实例。
下面是Changelog的一部分
1 | Version 5.6.11 |
上面的Changelog里,有一行很有意思。Bug #69892看起来很吓人,由于整数key截断导致不同数组之间比较时,结果相同。
Bug #69892 - 不同数组之间比较由于整数键截断导致结果相同
Bug的提交报告里的测试示例。
1 | [2015-06-20 14:29 UTC] nikic@php.net |
zend_hash_compare()
整数key截断问题发生在zend_hash_compare()
函数这里。
1 | ZEND_API int zend_hash_compare(HashTable *ht1, HashTable *ht2, compare_func_t compar, zend_bool ordered TSRMLS_DC) |
问题在于数字型下标在比较时,是通过各自的值相减结果来判断的,值是存放在bucket
类型的h
。通过判断结果是否为0决定是否相等。然而,h
的数据类型bucket
被定义为unsigned long
,在64位系统中,通常是64位,但是result
变量却只是32位int
类型。因此,当比较结果的低32位全是0的话,比较结果会是相等。因此,key=0
和key=4294967296
(0x10000000)以及其他的key都被认为相等。
5. 怎样解题?
我们现在已经能以用户5的身份登录,下一步只要将用户5变成用户0就可以了。
修改Cookie
1 | Cookie:user[4294967296]=5;user[1]=hund; |
在进行三等于判断时,由于bug的存在,会将user[4294967296]=5
当作user[0]=5
,成功验证
但是,在后面获取$uid
时,并没有进行比较,只是读取$user[0]
的值,因为$user[0]
并没有被初始化,所以当其加上0时,结果为0。这时,用户身份就从用户5变成了用户0,admin身份。
6. 补充
测试环境可以自己搭建,也可以使用在线的PHP沙箱:
这里附上在3v4l上测试的结果,由于无法设置Cookie,所以直接给$input
赋值
1 | $input = [4294967296 => "5","1" => 'hund']; |
为了方便看结果,还输出了$input
,可以看到只有在PHP5.4.0-5.4.43,5.5.0-5.5.26,5.6.0-5.6.1版本中才能以admin身份登录
漏洞的影响范围也是上面的版本