PHP内核层解析反序列化漏洞

Sl0th Lv4

PHP内核层解析反序列化漏洞

0x00 前置知识

魔法函数

  1. __construct() ,类的构造函数
  2. __destruct() ,类的析构函数
  3. __call() ,在对象中调用一个不可访问方法时调用
  4. __callStatic() ,在静态上下文中调用一个不可访问的方法时调用
  5. __get() ,读取不可访问属性的值时调用
  6. __set() ,在给不可访问属性赋值时调用
  7. __isset() ,当对不可访问属性调用isset()和empty()时,__isset() 被调用
  8. __unset() ,当对不可访问属性调用unset()时,__unset() 会被调用
  9. __sleep() ,执行serialize()时,先会调用这个函数
  10. __wakeup() ,执行unserialize()时,先会调用这个函数
  11. __toString() ,类被当成字符串时的回应方法
  12. __invoke() ,当尝试以调用函数的方式调用一个对象时,__invoke() 方法会被自动调用
  13. __set_state() ,当调用var_export()导出类时,此静态方法会被调用
  14. __clone() ,当对象复制完成时调用
  15. __debuginfo() ,当转储对象以获取应显示的属性时,会被调用
  16. __autoload() ,尝试加载未定义的类
  17. __serialize() ,执行serialize()时,先会调用这个函数**(这个和__sleep() 的区别后面会详细介绍)**
  18. __unserialize() ,执行unserialize()时,先会调用这个函数**(这个和__wakeup() 的区别后面会详细介绍)**

CVE-2016-7124

老生常谈的__wakeup绕过

题目背景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class test{
public $str;
function __destruct(){
$handle=fopen("xxx.php", "a+ ");
fwrite($handle, $this->str);
fclose($handle);
}
function __wakeup(){
$danger_arr-['eval','assert','call_user_func'];
$this->str=str_replace($danger_arr," , $this->str);
}
}
$o=unserialize('O:4:"test":1:{s:3:"str";s:10:"eval(aaaa)";}');

当一个类调用serialize进行序列化时会自动调用__sleep函数,当字符串要利用unserialize反序列化成一个类时会调用__wakeup函数。绕过方式就是老生常谈的O后数字大于实际类属性数目

0x01 源码解析

序列化

serialize函数定义在var.c文件中,进行了一些准备工作后,跟进php_var_serialize函数

1
2
3
4
5
PHPAPI void php_var_serialize(smart_str *buf, zval **struc, php_serialize_data_t *var_hash TSRMLS_DC) /* {{{ */
{
php_var_serialize_intern(buf, *struc, *var_hash TSRMLS_CC);
smart_str_0(buf);
}

跟进php_var_serialize_intern函数,关键部分如下

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
switch (Z_TYPE_P(struc)) {
case IS_BOOL:
smart_str_appendl(buf, "b:", 2);
smart_str_append_long(buf, Z_LVAL_P(struc));
smart_str_appendc(buf, ';');
return;

case IS_NULL:
smart_str_appendl(buf, "N;", 2);
return;

case IS_LONG:
php_var_serialize_long(buf, Z_LVAL_P(struc));
return;

case IS_DOUBLE: {
char *s;

smart_str_appendl(buf, "d:", 2);
s = (char *) safe_emalloc(PG(serialize_precision), 1, MAX_LENGTH_OF_DOUBLE + 1);
php_gcvt(Z_DVAL_P(struc), PG(serialize_precision), '.', 'E', s);
smart_str_appends(buf, s);
smart_str_appendc(buf, ';');
efree(s);
return;
}
.....

整个函数的结构是switch case,通过宏Z_TYPE_P解析struc变体的类型(此宏展开为struc->u1.v.type),来判断要序列化的类型,从而进入相应的CASE部分进行操作。

一般传入类序列化字符串,会被识别为OBJECT,进入IS_OBJECT分支

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
case IS_OBJECT: {
zval *retval_ptr = NULL;
zval fname;
int res;
zend_class_entry *ce = NULL;

if (Z_OBJ_HT_P(struc)->get_class_entry) {
ce = Z_OBJCE_P(struc);
}

if (ce && ce->serialize != NULL) {
/* has custom handler */
unsigned char *serialized_data = NULL;
zend_uint serialized_length;

if (ce->serialize(struc, &serialized_data, &serialized_length, (zend_serialize_data *)var_hash TSRMLS_CC) == SUCCESS) {
smart_str_appendl(buf, "C:", 2);
smart_str_append_long(buf, (int)Z_OBJCE_P(struc)->name_length);
smart_str_appendl(buf, ":\"", 2);
smart_str_appendl(buf, Z_OBJCE_P(struc)->name, Z_OBJCE_P(struc)->name_length);
smart_str_appendl(buf, "\":", 2);

smart_str_append_long(buf, (int)serialized_length);
smart_str_appendl(buf, ":{", 2);
smart_str_appendl(buf, serialized_data, serialized_length);
smart_str_appendc(buf, '}');
} else {
smart_str_appendl(buf, "N;", 2);
}
if (serialized_data) {
efree(serialized_data);
}
return;
}

if (ce && ce != PHP_IC_ENTRY && zend_hash_exists(&ce->function_table, "__sleep", sizeof("__sleep"))) {//调用__sleep
INIT_PZVAL(&fname);
ZVAL_STRINGL(&fname, "__sleep", sizeof("__sleep") - 1, 0);
BG(serialize_lock)++;
res = call_user_function_ex(CG(function_table), &struc, &fname, &retval_ptr, 0, 0, 1, NULL TSRMLS_CC);
BG(serialize_lock)--;

if (EG(exception)) {
if (retval_ptr) {
zval_ptr_dtor(&retval_ptr);
}
return;
}

if (res == SUCCESS) {
if (retval_ptr) {
if (HASH_OF(retval_ptr)) {
php_var_serialize_class(buf, struc, retval_ptr, var_hash TSRMLS_CC);
} else {
php_error_docref(NULL TSRMLS_CC, E_NOTICE, "__sleep should return an array only containing the names of instance-variables to serialize");
/* we should still add element even if it's not OK,
* since we already wrote the length of the array before */
smart_str_appendl(buf,"N;", 2);
}
zval_ptr_dtor(&retval_ptr);
}
return;
}
}

if (retval_ptr) {
zval_ptr_dtor(&retval_ptr);
}
/* fall-through */
}

因上面case IS_OBJECT分支中没有流程命中,case中又没有break语句,继续执行进入IS_ARRAY分支,在这里从struc结构中提取出类名,计算其长度并赋值到buf结构中,并提取出类中要序列化的结构存入哈希数组中。

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
case IS_ARRAY: {
zend_bool incomplete_class = 0;
if (Z_TYPE_P(struc) == IS_ARRAY) {
smart_str_appendl(buf, "a:", 2);
myht = HASH_OF(struc);
} else {
incomplete_class = php_var_serialize_class_name(buf, struc TSRMLS_CC);
myht = Z_OBJPROP_P(struc);
}
/* count after serializing name, since php_var_serialize_class_name
* changes the count if the variable is incomplete class */
i = myht ? zend_hash_num_elements(myht) : 0;
if (i > 0 && incomplete_class) {
--i;
}
smart_str_append_long(buf, i);
smart_str_appendl(buf, ":{", 2);
if (i > 0) {
char *key;
zval **data;
ulong index;
uint key_len;
HashPosition pos;

zend_hash_internal_pointer_reset_ex(myht, &pos);
for (;; zend_hash_move_forward_ex(myht, &pos)) {
i = zend_hash_get_current_key_ex(myht, &key, &key_len, &index, 0, &pos);
if (i == HASH_KEY_NON_EXISTENT) {
break;
}
if (incomplete_class && strcmp(key, MAGIC_MEMBER) == 0) {
continue;
}

switch (i) {
case HASH_KEY_IS_LONG:
php_var_serialize_long(buf, index);
break;
case HASH_KEY_IS_STRING:
php_var_serialize_string(buf, key, key_len - 1);
break;
}

/* we should still add element even if it's not OK,
* since we already wrote the length of the array before */
if (zend_hash_get_current_data_ex(myht, (void **) &data, &pos) != SUCCESS
|| !data
|| data == &struc
|| (Z_TYPE_PP(data) == IS_ARRAY && Z_ARRVAL_PP(data)->nApplyCount > 1)
) {
smart_str_appendl(buf, "N;", 2);
} else {
if (Z_TYPE_PP(data) == IS_ARRAY) {
Z_ARRVAL_PP(data)->nApplyCount++;
}
php_var_serialize_intern(buf, *data, var_hash TSRMLS_CC);
if (Z_TYPE_PP(data) == IS_ARRAY) {
Z_ARRVAL_PP(data)->nApplyCount--;
}
}
}
}
smart_str_appendc(buf, '}');
return;
}
default:
smart_str_appendl(buf, "i:0;", 4);
return;
}

接下来就是利用php_var_serialize_intern函数递归解析整个哈希数组的过程,从中分别提取出变量名和值进行格式解析并将解析完成的字符串拼接到buf结构中。最后当整个过程结束后,整个字符串讲完全存进柔性数组结构buf中。

__sleep的情况

跟进php_var_serialize_call_sleep函数

image-20230322164744700
image-20230322164744700

我们这里继续跟进call_user_function,根据宏定义,它实际上是调用了_call_user_function_ex函数,在这里做了一些拷贝动作,故不做截图,流程接下来进入zend_call_function函数的调用。

函数zend_call_function中,实际情况下,在__sleep中需要做一些我们自己的事情,这里PHP将要做的操作压入PHP自己的zend_vm引擎堆栈中,稍后会进行一条条解析(就是解析相应的OPCODE)。

这里流程会命中此分支,我们跟进zend_execute_ex函数。

image-20230322164936378
image-20230322164936378

我们这里可以看到在ZEND_VM中,整体体处理流程为while(1)循环,不断解析ZEND_VM栈中的操作。上图红框中ZEND_VM引擎会利用ZEND_FASTCALL方式派发到到相应的处理函数。在处理完_sleep函数中的一系列操作之后,接下来用php_var_serialize_class函数来序列化类名,递归序列化其_sleep函数返回值中的结构。最终都把结果存在了buf结构中。至此序列化的整个流程完毕。

小结

当没有魔法函数时,序列化类名–>利用递归序列化剩下的结构

当存在魔法函数时,调用魔法函数__sleep–>利用ZEND_VM引擎解析PHP操作—>返回需要序列化结构的数组–>序列化类名–>利用递归序列化__sleep的返回值结构。

反序列化

php7中,加入了新特性,带过滤的反序列化,根据allowed_calsses的设置情况来过滤相应的php对象

以下为php5的源码

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
PHP_FUNCTION(unserialize)
{
char *buf = NULL;
int buf_len;
const unsigned char *p;
php_unserialize_data_t var_hash;

if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &buf, &buf_len) == FAILURE) {
RETURN_FALSE;
}

if (buf_len == 0) {
RETURN_FALSE;
}

p = (const unsigned char*) buf;
PHP_VAR_UNSERIALIZE_INIT(var_hash);
if (!php_var_unserialize(&return_value, &p, p + buf_len, &var_hash TSRMLS_CC)) {
PHP_VAR_UNSERIALIZE_DESTROY(var_hash);
zval_dtor(return_value);
if (!EG(exception)) {
php_error_docref(NULL TSRMLS_CC, E_NOTICE, "Error at offset %ld of %d bytes", (long)((char*)p - buf), buf_len);
}
RETURN_FALSE;
}
PHP_VAR_UNSERIALIZE_DESTROY(var_hash);
}

被过滤的对象会被转化成__PHP_Incomplete_Class对象不能被直接使用,但是这里对反序列化流程没有影响,这里不做详细探讨。我们跟进php_var_unserialize函数。

image-20230322170306309
image-20230322170306309

继续跟进php_var_unserialize_internal函数。

image-20230322170416302
image-20230322170416302

此函数内部主要操作流程为对字符串进行解析,然后跳转到相应的处理流程。上图中解析出第一个字母0,代表此次反序列化为一个对象。这里首先会解析出对象名字,并进行查表操作确定此对象确实存在

image-20230322170506357
image-20230322170506357

上述操作做完之后,我们这里根据对象名称new出了自己新的对象并进行了初始化,但是我们的反序列化操作还是没有完成,我们跟进object_common2函数。

在这里我们看到了对魔法函数的判断与检测,但是调用部分并不在此。我们继续跟进process_nested_data函数。

image-20230322170613770
image-20230322170613770

看来这个函数利用WHILE循环来嵌套解析剩余的部分了,·其中包含两个php_var_unserialize_internal函数,第一个会解析名称,第二个是解析名称所对应的值。process_nested_data函数运行完毕后,字符串解析完毕,反序列化操作主要内容已经完成,流程即将进入尾声了。

在序列化完成后,在PHP_VAR_UNSERIALIZE_DESTROY释放空间处出现了__wakeup

image-20230322170811880
image-20230322170811880

还记得反序列化流程中当发现有__wakeup时对其进行的VAR_WAKEUP_FLAG标志么,在这里当遍历bar_dtor_hash数组遇到这个标志时,正式开启对__wakeup调用,后期的调用手法和前面所介绍的__sleep调用手法完全相同,这里不再做重复说明。至此,反序列化所有流程完毕。

小结:

获取反序列化字符串–>根据类型进行反序列化—>查表找到对应的反序列化类–>根据字符串判断元素个数–>new出新实例–>迭代解析化剩下的字符串–>判断是否具有魔法函数__wakeup并标记—>释放空间并判断是否具有具有标记—>开启调用

0x02 漏洞解析

漏洞CVE-2016-7124(绕过__wakeup)魔法函数

触发手段

字符串中代表元素个数的数值大于真实值

1
'O:4:"test":2:{s:1:"a";s:5:"hello";}'

其他方式

1
2
3
'O:4:"test":1:{s:2:"a";s:5:"hello";}'
'O:4:"test":1:{d:1:"a";s:5:"hello";}'
'O:4:"test":1:{s:1:"a";s:6:"hello";}'

只要在解析类中的元素出现错误时,都会触发此漏洞。但是更改类元素内部操作(如上图的修改字符串长度,类变量类型等)会导致类成员变量赋值失败。只有修改类成员的个数(比原有成员个数大)时,才能保证类成员赋值时成功的。

低版本的处理过程现在看来也相对简略。但是整体谐逻辑并没有改变,我们这里直接跟进php_var_unserialize函数,此后相同逻辑不再进行重复说明,我们直接跟到差异处(object_common2函数)也就是处理类中成员变量的代码

image-20230322171924054
image-20230322171924054

在函数object_common2中,存在两个主要操作,process_nested_data迭代解析类中的数据和魔法函数__wakeup的调用,且当process_nested_data函数解析失败后,直接返回0值,后面的__wakeup函数将没有调用的机会。

image-20230322171947861
image-20230322171947861

当只修改类成员的个数时,while循环可以完成的进行一次,这使得我们类中成员变量能被完整的赋值。当修改成员变量内部时,pap_var_unserialize函数调用失败,紧接着会调用zval_dtorFREE_ZVAL函数释放当前key(变量)空间,导致类中的变量赋值失败。

  • 标题: PHP内核层解析反序列化漏洞
  • 作者: Sl0th
  • 创建于 : 2023-07-05 13:08:07
  • 更新于 : 2024-11-11 18:23:06
  • 链接: http://sl0th.top/2023/07/05/PHP内核层解析反序列化漏洞/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论