内容简介:一、概述近日,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。
一、概述
近日,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
<a href="/cdn-cgi/l/email-protection" data-cfemail="493b26263d092839283c2b3c273d3c">[email protected]</a>:~# 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结构为目标
<a href="/cdn-cgi/l/email-protection" data-cfemail="14667b7b60547564756176617a6061">[email protected]</a>:~# 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)
(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是字符串。
如我们所见,两个最左边的结构都是叠加的。
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) 使用每个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服务器。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 漏洞分析:OpenSSH用户枚举漏洞(CVE-2018-15473)分析
- 【漏洞分析】CouchDB漏洞(CVE–2017–12635, CVE–2017–12636)分析
- 【漏洞分析】lighttpd域处理拒绝服务漏洞环境从复现到分析
- 漏洞分析:对CVE-2018-8587(Microsoft Outlook)漏洞的深入分析
- 路由器漏洞挖掘之 DIR-815 栈溢出漏洞分析
- Weblogic IIOP反序列化漏洞(CVE-2020-2551) 漏洞分析
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。