Challenge 1:phpBug #69892

前言

今天才看到群里需要交作业,是要审计一道 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 中的例子极其简单就只有一段代码,大体意思就是当数组的键 016 进制的 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 发生在结构 bucketh 元素被定义为 unsigned long,这在64位系统上的通常是64位,但是最终变量只是一个32位的 int 数据类型。所以,这种比较不仅适用于 h 元素值相同的情况,还适用于两个元素相减结果是32位下的 0。

所以,键 0 和 键 4294967296 还有许多其他键(1099511627776、281474976710656、等等)相比较时都会是 true

所以这里传 user[281474976710656]=5;user[1]=hund281474976710656 对应的值将会是:0x1000000000000

此时已经成功进入 if($input === $user) 语句内了, 这个时候 $input[0] 的值是 null,当值进行 + 0
后,类型被强制成整型了值才就变成了整型 0。(null 和 0 进行弱类型比较时会等于 true 的,这里 + 0 是不是不应该呀?)

最终我们成功到达输出 Admin 身份的部分:


版权声明

除非另有说明,本网站上的内容均根据 Creative Commons Attribution-ShareAlike License 4.0 International (CC BY-SA 4.0) 获得许可。