什么是序列化以及反序列化?

序列化是将PHP中的值(zval)转换成一段包含字节流的字符串。 序列化一个对象会保存对象的所有变量的值,但是不会保存对象的方法,只会保存类的名字。

反序列化:对单一的已序列化的变量进行操作,将其转换回 PHP 的值(zval)。

PHP序列化方式

PHP在序列化的时候会将相应的变量以对应的键值进行储存。

将一个类序列化的话,处理代码主要的文件:ext/standard/var.c中,如下。

php_var_serialize_class()函数:

1
2
3
4
5
static void php_var_serialize_class(smart_str *buf, zval *struc, zval *retval_ptr, HashTable *var_hash TSRMLS_DC) /* {{{ */
{
...
incomplete_class = php_var_serialize_class_name(buf, struc TSRMLS_CC);
...

php_var_serialize_class_name()函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
static inline zend_bool php_var_serialize_class_name(smart_str *buf, zval *struc TSRMLS_DC) /* {{{ */
{
PHP_CLASS_ATTRIBUTES;
PHP_SET_CLASS_ATTRIBUTES(struc);
smart_str_appendl(buf, "O:", 2);
smart_str_append_long(buf, (int)name_len);
smart_str_appendl(buf, ":\"", 2);
smart_str_appendl(buf, class_name, name_len);
smart_str_appendl(buf, "\":", 2);
PHP_CLEANUP_CLASS_ATTRIBUTES();
return incomplete_class;
}

需要序列化一个类的话,首先PHP会先将类名序列化。格式为

O:类名长度:”类名”:值:{}

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class test
{
public function show_one()
{
echo $this->one;
}
public function show_two()
{
echo "123";
}
}

例:如果一个类名叫做test的类没有定义任何变量的话,序列化之后结果如下:

1
O:4:"test":0:{}

我们可以看到,这个类中的方法没有在序列化字符串中出现,也体现了开头的“序列化一个对象将会保存对象的所有变量,但是不会保存对象的方法,只会保存类的名字。”。

其中还有比较特殊的序列化就是数组中的引用(&)的序列化与实例化后对象中自身的二次赋值。

我们在这用PHP Internal Book中的例子。

例1:

1
2
3
4
5
6
7
8
9
10
11
<?php
/**
* User: LonelyRain
*/
$a = ["foo"];
$a[1] =& $a[0];
$s = serialize($a);
print $s;

以上代码的序列化结果是

1
a:2:{i:0;s:3:"foo";i:1;R:2;}

这里的R:2;部分意味着”指向第二个值”.什么是第二个值?整个数组代表第一个值, (s:3:”foo”) 代表第二个值.

1
2
3
4
5
6
7
8
9
10
11
<?php
/**
* User: LonelyRain
*/
$o = new stdClass;
$o->foo = $o;
$s = serialize($o);
print $s;

以上代码的序列化结果是

1
O:8:"stdClass":1:{s:3:"foo";r:1;}

以下是zval对应的类型和键对照表

1
2
3
4
5
6
7
8
9
10
11
12
13
序列化键名对照表:
数组中二次赋值(&): R;
对象二次赋值 : r;
NULL : N;
true : b:1;
false : b:0;
Long : i;
Double : d;
String : s/S;
Class : C;
Array : a;
Object : O;

变量不同的属性也有着不同的格式

1
2
3
public : key;
protected : \0*\0key;
private : \0key\0;

通过实例来观察:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
/**
* User: LonelyRain
*/
class Test {
public $public = 1;
protected $protected = 2;
private $private = 3;
}
$a = new Test();
$s = serialize($a);
var_dump($s);

结果:

1
"O:4:"Test":3:{s:6:"public";i:1;s:12:"\0*\0protected";i:2;s:13:"\0Test\0private";i:3;}"

再来看一看反序列化的相关知识。大家应该注意到了String对应着两个键,s与S。

serialize()与unserialize()处理有着一些差异。PHP源码serialize()中是没有相关序列化是以S为标识的,但是在unserialize中又有对S键的相关处理,下面我把相关部分代码贴出来供读者参考。

1
2
3
4
5
6
7
8
9
10
11
12
13
case 'S': goto yy10;
...
yy10:
yych = *(YYMARKER = ++YYCURSOR);
if (yych == ':') goto yy39;
goto yy3;
...
yy39:
yych = *++YYCURSOR;
if (yych == '+') goto yy40;
if (yych <= '/') goto yy18;
if (yych <= '9') goto yy41;
goto yy18;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
case 's': goto yy9;
...
yy9:
yych = *(YYMARKER = ++YYCURSOR);
if (yych == ':') goto yy46;
goto yy3;
...
yy46:
yych = *++YYCURSOR;
if (yych == '+') goto yy47;
if (yych <= '/') goto yy18;
if (yych <= '9') goto yy48;
goto yy18;
...
...

如果大家继续看接下去的代码下去,会发现s和S的就会发现两个键的处理方式是一模一样的。

如果大家看了phpcodz 10,里面写道a:1:{s:8:"ryatsyne"tO:8:"ryatsyne":0:{}}这样可以突破

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static public function safeUnserialize( $serialized )
{
// unserialize will return false for object declared with small cap o
// as well as if there is any ws between O and :
if ( is_string( $serialized ) && strpos( $serialized, "\0" ) === false )
{
if ( strpos( $serialized, 'O:' ) === false )
{
// the easy case, nothing to worry about
// let unserialize do the job
return @unserialize( $serialized );
}
else if ( ! preg_match('/(^|;|{|})O:[+\-0-9]+:"/', $serialized ) )
{
// in case we did have a string with O: in it,
// but it was not a true serialized object
return @unserialize( $serialized );
}
}
return false;
}

这个payload在php5.6.23中失效,看以下代码

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
yy48:
++YYCURSOR;
if ((YYLIMIT - YYCURSOR) < 2) YYFILL(2);
yych = *YYCURSOR;
if (yych <= '/') goto yy18;
if (yych <= '9') goto yy48;
if (yych >= ';') goto yy18;
yych = *++YYCURSOR;
if (yych != '"') goto yy18;
++YYCURSOR;
{
size_t len, maxlen;
char *str;
len = parse_uiv(start + 2);
maxlen = max - YYCURSOR;
if (maxlen < len) {
*p = start + 2;
return 0;
}
str = (char*)YYCURSOR;
YYCURSOR += len;
if (*(YYCURSOR) != '"') {
*p = YYCURSOR;
return 0;
}
if (*(YYCURSOR + 1) != ';') {
*p = YYCURSOR + 1;
return 0;
}
YYCURSOR += 2;
*p = YYCURSOR;
INIT_PZVAL(*rval);
ZVAL_STRINGL(*rval, str, len, 1);
return 1;
}

代码中已经多加了分号符号校验,这个tricky在这个php版本中是无效的。

1
2
3
4
5
if (*(YYCURSOR + 1) != ';') {
*p = YYCURSOR + 1;
return 0;
}

WDDX序列化方式

序列化本质就是将程序的值以相应的格式保存下来,所以我们不止单单可以用上面的serialize函数进行序列化。PHP还提供了另外一种序列化格式为Web分布式数据交换(WDDX)。WDDX是XML的子集,所以符合WDDX的序列化过后的字符串格式是符合xml的规范的。

演示代码:

1
2
3
4
5
6
7
8
9
10
<?php
/**
* User: LonelyRain
*/
$a = ["foo"];
$a[1] =& $a[0];
echo wddx_serialize_value($a);
?>

结果:

1
<wddxPacket version='1.0'><header/><data><array length='2'><string>foo</string><string>foo</string></array></data></wddxPacket>

可以看到才用wddx_serialize_value()函数处理的$a和之前使用serialize()函数处理的值都被保存下来了,只不过遵守的格式有着相应的区别。

WDDX序列化反序列化相关函数:

1
2
3
4
5
6
wddx_serialize_value: 将单一值连续化。
wddx_serialize_vars : 将多值连续化。
wddx_packet_start : 开始新的 WDDX 封包。
wddx_packet_end : 结束的 WDDX 封包。
wddx_add_vars : 将 WDDX 封包连续化。
wddx_deserialize : 将 WDDX 封包解连续化。

这一篇主要讲了序列化后数据的格式,下一次会写PHP序列化中一块重要的内容,PHP的魔术方法等内容。

Reference:

PHP内核
PHP string序列化与反序列化语法解析不一致带来的安全隐患
PHP中文手册

Comments

⬆︎TOP