前言
今天才看到群里需要交作业,是要审计一道 CTF 中的 WEB 题。起初以为没难度的,然后发现思路走偏了。。。
查看bugs
看到题目名第一反应是去 bugs.php.net 查看对应错误编号:
Bug #69892 Different arrays compare indentical due to integer key truncation
Submitted: 2015-06-20 14:29 UTC Modified: 2015-06-20 14:29 UTC
From: nikic@php.net Assigned: nikic (profile)
Status: Closed Package: Scripting Engine problem
PHP Version: 5.5.26 OS:
Private report: No CVE-ID: None
Description:
------------
var_dump([0 => 0] === [0x100000000 => 0]); // bool(true)
然后再去看代码:
<?php
$users = array(
"0:9b5c3d2b64b8f74e56edec71462bd97a",
"1:4eb5fb1501102508a86971773849d266",
"2:facabd94d57fc9f1e655ef9ce891e86e",
"3:ce3924f011fe323df3a6a95222b0c909",
"4:7f6618422e6a7ca2e939bd83abde402c",
"5:06e2b745f3124f7d670f78eabaa94809",
"6:8e39a6e40900bb0824a8e150c0d0d59f",
"7:d035e1a80bbb377ce1edce42728849f2",
"8:0927d64a71a9d0078c274fc5f4f10821",
"9:e2e23d64a642ee82c7a270c6c76df142",
"10:70298593dd7ada576aff61b6750b9118",
);
$valid_user = false;
$input = $_COOKIE['user'];
$input[1] = md5($input[1]);
foreach ($users as $user) {
$user = explode(":", $user);
if ($input === $user) {
$uid = $input[0] + 0;
$valid_user = true;
}
}
if (!$valid_user) {
die("not a valid user\n");
}
if ($uid == 0) {
echo "Hello Admin How can I serve you today?\n";
echo "SECRETS ....\n";
} else {
echo "Welcome back user\n";
}
说实话,这两个东西结合在一起时第一眼还真没看懂,后面调试到 $uid == 0
才明白意思。
调试及验证
首先逐步分析代码逻辑:
- 接收 cookie 中的 user 参数,循环对比转化后的值是否一致
- 三个判断,识别用户最终进入 admin 身份
获取用户身份
这一步比较简单,代码中存在一个 $users
数组,里面的值均为 md5 转换后的内容。将这个数组里的值逐行与接收到的 cookie 参数进行比对,如果结果一致则将 $valid_user
参数赋值成 true。这样就进入了 $uid == 0
判断语句。
首先我们将这多条 md5 扔给在线解密网站,发现只成功解密出了一条:06e2b745f3124f7d670f78eabaa94809:hund
此时我们将该参数带入 cookie 中请求一下,cookie 参数:user[]=5;user[]=hund
。
可以看到请求回写了:Welcome back user,现在只要在满足 $uid == 0
这个判断条件即可。
数组整数键截断问题
看 bugs 中的例子极其简单就只有一段代码,大体意思就是当数组的键 0 与 16 进制的 0x100000000 进行全等比对时,会返回 true。
简单验证下不同版本下的输出:
可以看到 7.2.1 版下返回 false,而 5.2.17 无结果返回表明存在溢出。
我们都知道 PHP 中可以不指定索引值往数组中添加元素,这个时会默认使用数字作为索引,如果下一个依然没有指定索引key,将会用 key+1 后的数字 进行填充。
所以这里的全等匹配,必须要数组的索引满足 0x100000000 就可以了。那么应该传什么呢?
为了搞懂是为什么,我翻了很久的gg去总结
在 PHP 内核中有个重要的数据结构 hashtable ,我们常用到的的数组,在内核中就是用 hashtable 来实现。
查看 PHP 的 zend 代码定位到问题产生处(php-src/Zend/zend_hash.c):
我们在看看 Bucket 的定义(php-src/Zend/zend_hash.h):
typedef struct bucket {
ulong h; /* 哈希值(或数字键值的key)*/
uint nKeyLength; /* key的长度 */
void *pData; /* 指向数据的指针 */
void *pDataPtr; /* 指针数据 */
struct bucket *pListNext; /* 指向HashTable中的arBuckets链表中的下一个元素 */
struct bucket *pListLast; /* 指向HashTable中的arBuckets链表中的上一个元素 */
struct bucket *pNext; /* 指向具有相同hash值的bucket链表中的下一个元素 */
struct bucket *pLast; /* 指向具有相同hash值的bucket链表中的上一个元素 */
const char *arKey; /* key的名称 */
} Bucket;
图中可以看到 result = p1->h - p2->h;
,数值索引是通过减去双方的值来进行比较的,这个值存储在 bucket 数据类型的 h 元素中。如果 result 结果不为 0,则检测到存在差异。
这个 bug 发生在结构 bucket 的 h 元素被定义为 unsigned long,这在64位系统上的通常是64位,但是最终变量只是一个32位的 int 数据类型。所以,这种比较不仅适用于 h 元素值相同的情况,还适用于两个元素相减结果是32位下的 0。
所以,键 0 和 键 4294967296 还有许多其他键(1099511627776、281474976710656、等等)相比较时都会是 true。
所以这里传 user[281474976710656]=5;user[1]=hund
,281474976710656 对应的值将会是:0x1000000000000
此时已经成功进入 if($input === $user)
语句内了, 这个时候 $input[0]
的值是 null,当值进行 + 0
后,类型被强制成整型了值才就变成了整型 0。(null 和 0 进行弱类型比较时会等于 true 的,这里 + 0 是不是不应该呀?)
最终我们成功到达输出 Admin
身份的部分: