PHP Unserialize Feature on PHP7

在PHP7.1+的反序列化操作中,对于类的私有属性增加了一些“纠错”处理,这就导致了在反序列化的过程中,即便以public属性进行反序列化,最后也会按照类本来声明的类型,反序列化为私有属性,即对反序类化属性类型不敏感

问题的开始还要从2020网鼎杯青龙组的AreUSerialz说起。

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
<?php

include("flag.php");

highlight_file(__FILE__);

class FileHandler {

protected $op;
protected $filename;
protected $content;

function __construct() {
$op = "1";
$filename = "/tmp/tmpfile";
$content = "Hello World!";
$this->process();
}

public function process() {
if($this->op == "1") {
$this->write();
} else if($this->op == "2") {
$res = $this->read();
$this->output($res);
} else {
$this->output("Bad Hacker!");
}
}

private function write() {
if(isset($this->filename) && isset($this->content)) {
if(strlen((string)$this->content) > 100) {
$this->output("Too long!");
die();
}
$res = file_put_contents($this->filename, $this->content);
if($res) $this->output("Successful!");
else $this->output("Failed!");
} else {
$this->output("Failed!");
}
}

private function read() {
$res = "";
if(isset($this->filename)) {
$res = file_get_contents($this->filename);
}
return $res;
}

private function output($s) {
echo "[Result]: <br>";
echo $s;
}

function __destruct() {
if($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();
}

}

function is_valid($s) {
for($i = 0; $i < strlen($s); $i++)
if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
return false;
return true;
}

if(isset($_GET{'str'})) {

$str = (string)$_GET['str'];
if(is_valid($str)) {
$obj = unserialize($str);
}

}

这个题的代码和思路都不难,反序列化构造任意文件读取,主要问题在于需要绕过is_valid。由于protect类型的属性在序列化的字符串中,会带有不可见字\x00,无法通过检测,需要绕过。

常规思路是用S来替代s,从而保证序列化的字符串均在可见字符范围内。
但是有意思的是,由于这个题目的环境是php7.3,使用public进行构造即可保证序列化的字符串满足要求,且不出现报错,成功反序列化。

关于这一点,在php的官方文档中没有找到相关描述,于是乎,从源码入手。
7.0.337.3.26 为例。

首先来看 7.3.26,对类属性类型的处理在ext/standard/var_unserializer.c中的process_nested_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
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
#define UNSERIALIZE_PARAMETER zval *rval, const unsigned char **p, const unsigned char *max, php_unserialize_data_t *var_hash
#define UNSERIALIZE_PASSTHRU rval, p, max, var_hash

static zend_always_inline int process_nested_data(UNSERIALIZE_PARAMETER, HashTable *ht, zend_long elements, int objprops)
{
while (elements-- > 0) {
zval key, *data, d, *old_data;
zend_ulong idx;

ZVAL_UNDEF(&key);

if (!php_var_unserialize_internal(&key, p, max, NULL, 1)) {
zval_ptr_dtor(&key);
return 0;
}

data = NULL;
ZVAL_UNDEF(&d);

if (!objprops) {
if (Z_TYPE(key) == IS_LONG) {
idx = Z_LVAL(key);
numeric_key:
if (UNEXPECTED((old_data = zend_hash_index_find(ht, idx)) != NULL)) {
//??? update hash
var_push_dtor(var_hash, old_data);
data = zend_hash_index_update(ht, idx, &d);
} else {
data = zend_hash_index_add_new(ht, idx, &d);
}
} else if (Z_TYPE(key) == IS_STRING) {
if (UNEXPECTED(ZEND_HANDLE_NUMERIC(Z_STR(key), idx))) {
goto numeric_key;
}
if (UNEXPECTED((old_data = zend_hash_find(ht, Z_STR(key))) != NULL)) {
//??? update hash
var_push_dtor(var_hash, old_data);
data = zend_hash_update(ht, Z_STR(key), &d);
} else {
data = zend_hash_add_new(ht, Z_STR(key), &d);
}
} else {
zval_ptr_dtor(&key);
return 0;
}
} else {
//从这里开始看
//从这里开始看
//从这里开始看
if (EXPECTED(Z_TYPE(key) == IS_STRING)) {
string_key:
if (Z_TYPE_P(rval) == IS_OBJECT
&& zend_hash_num_elements(&Z_OBJCE_P(rval)->properties_info) > 0) {
zend_property_info *existing_propinfo;
zend_string *new_key;
const char *unmangled_class = NULL;
const char *unmangled_prop;
size_t unmangled_prop_len;
zend_string *unmangled;

if (UNEXPECTED(zend_unmangle_property_name_ex(Z_STR(key), &unmangled_class, &unmangled_prop, &unmangled_prop_len) == FAILURE)) {
/*
zend_unmangle_property_name_ex对属性名进行解析,判断首字符是否为\0,不为\0则是公有属性,unmangled_class = NULL;
其他则判断为私有属性,并进行合法性检测,unmangled_class = ZSTR_VAL(Z_STR(key)) + 1,不合法返回FAILURE
unmangled_prop: 属性名称
*/
zval_ptr_dtor(&key);
return 0;
}

unmangled = zend_string_init(unmangled_prop, unmangled_prop_len, 0); //属性名称

existing_propinfo = zend_hash_find_ptr(&Z_OBJCE_P(rval)->properties_info, unmangled);
//从HashTable properties_info中找到对应的属性信息

if ((unmangled_class == NULL || !strcmp(unmangled_class, "*") || !strcasecmp(unmangled_class, ZSTR_VAL(Z_OBJCE_P(rval)->name)))
&& (existing_propinfo != NULL)
&& (existing_propinfo->flags & ZEND_ACC_PPP_MASK)) {
if (existing_propinfo->flags & ZEND_ACC_PROTECTED) {
//处理protect属性
//默认注册protect属性
new_key = zend_mangle_property_name(
"*", 1, ZSTR_VAL(unmangled), ZSTR_LEN(unmangled), 0);
zend_string_release_ex(unmangled, 0);
} else if (existing_propinfo->flags & ZEND_ACC_PRIVATE) {
//处理private属性
if (unmangled_class != NULL && strcmp(unmangled_class, "*") != 0) {
//非NULL为私有属性,进入常规处理流程
new_key = zend_mangle_property_name(
unmangled_class, strlen(unmangled_class),
ZSTR_VAL(unmangled), ZSTR_LEN(unmangled),
0);
} else {
//unmangled_class为NULL,进入纠错处理
new_key = zend_mangle_property_name(
ZSTR_VAL(existing_propinfo->ce->name), ZSTR_LEN(existing_propinfo->ce->name),
ZSTR_VAL(unmangled), ZSTR_LEN(unmangled),
0);//从existing_propinfo->ce获取类名
}
zend_string_release_ex(unmangled, 0);
} else {
//处理public
ZEND_ASSERT(existing_propinfo->flags & ZEND_ACC_PUBLIC);
new_key = unmangled;
}
zval_ptr_dtor_str(&key);
ZVAL_STR(&key, new_key);
} else {
zend_string_release_ex(unmangled, 0);
}
}

if ((old_data = zend_hash_find(ht, Z_STR(key))) != NULL) {
if (Z_TYPE_P(old_data) == IS_INDIRECT) {
old_data = Z_INDIRECT_P(old_data);
}
var_push_dtor(var_hash, old_data);
data = zend_hash_update_ind(ht, Z_STR(key), &d);
} else {
data = zend_hash_add_new(ht, Z_STR(key), &d);
}
} else if (Z_TYPE(key) == IS_LONG) {
/* object properties should include no integers */
convert_to_string(&key);
goto string_key;
} else {
zval_ptr_dtor(&key);
return 0;
}
}

if (!php_var_unserialize_internal(data, p, max, var_hash, 0)) {
zval_ptr_dtor(&key);
return 0;
}

var_push_dtor(var_hash, data);
zval_ptr_dtor_str(&key);

if (elements && *(*p-1) != ';' && *(*p-1) != '}') {
(*p)--;
return 0;
}
}

return 1;
}

通过上述代码逻辑,可以发现,在反序列化的过程中,当解析属性时,属性类型是从声明处获取的,然后再分别针对不同类型进行处理,而不是从反序列化串中获取的,所以出现了属性类型不敏感的黑魔法。

而7.0.33中,没有对属性类型分别进行判断处理的代码:

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
static zend_always_inline int process_nested_data(UNSERIALIZE_PARAMETER, HashTable *ht, zend_long elements, int objprops)
{
while (elements-- > 0) {
zval key, *data, d, *old_data;
zend_ulong idx;

ZVAL_UNDEF(&key);

if (!php_var_unserialize_internal(&key, p, max, NULL, classes)) {
zval_dtor(&key);
return 0;
}

data = NULL;
ZVAL_UNDEF(&d);

if (!objprops) {
if (Z_TYPE(key) == IS_LONG) {
idx = Z_LVAL(key);
numeric_key:
if (UNEXPECTED((old_data = zend_hash_index_find(ht, idx)) != NULL)) {
//??? update hash
var_push_dtor(var_hash, old_data);
data = zend_hash_index_update(ht, idx, &d);
} else {
data = zend_hash_index_add_new(ht, idx, &d);
}
} else if (Z_TYPE(key) == IS_STRING) {
if (UNEXPECTED(ZEND_HANDLE_NUMERIC(Z_STR(key), idx))) {
goto numeric_key;
}
if (UNEXPECTED((old_data = zend_hash_find(ht, Z_STR(key))) != NULL)) {
//??? update hash
var_push_dtor(var_hash, old_data);
data = zend_hash_update(ht, Z_STR(key), &d);
} else {
data = zend_hash_add_new(ht, Z_STR(key), &d);
}
} else {
zval_dtor(&key);
return 0;
}
} else {
if (EXPECTED(Z_TYPE(key) == IS_STRING)) {
string_key:
if ((old_data = zend_hash_find(ht, Z_STR(key))) != NULL) {
if (Z_TYPE_P(old_data) == IS_INDIRECT) {
old_data = Z_INDIRECT_P(old_data);
}
var_push_dtor(var_hash, old_data);
data = zend_hash_update_ind(ht, Z_STR(key), &d);
} else {
data = zend_hash_add_new(ht, Z_STR(key), &d);
}
} else if (Z_TYPE(key) == IS_LONG) {
/* object properties should include no integers */
convert_to_string(&key);
goto string_key;
} else {
zval_dtor(&key);
return 0;
}
}

if (!php_var_unserialize_internal(data, p, max, var_hash, classes)) {
zval_dtor(&key);
return 0;
}

var_push_dtor(var_hash, data);
zval_dtor(&key);

if (elements && *(*p-1) != ';' && *(*p-1) != '}') {
(*p)--;
return 0;
}
}

return 1;
}

以下是本文所用到的一些参考资料,涉及到上述代码中所使用的结构体和常量:


PHP Unserialize Feature on PHP7
https://250.ac.cn/2021/01/11/php-unserialize-feature-on-php7/
Author
惊蛰
Posted on
January 11, 2021
Licensed under