项目 | 描述 |
---|---|
PHP | 7.4.9 |
在本系列文章(PHP 反序列化漏洞)中,做出如下约定:
数据结构
定义为 PHP 中的各种数据结构,如对象、字符串、数组等结构,具体而言数据结构即 数据的组织和表示方式
。
数据格式
定义为 数据结构的具化
,数据格式与数据结构两者的关系可以和 类与对象
的关系进行类比,数据结构是数据的组织和表示方式,而数据格式则可以认为是数据结构的具化,是数据结构的 具化(具体实现)
。
序列化操作的 目标
是 PHP 中的 各种数据结构(对象、数组及字符串等)
,序列化操作
可以理解为 对数据结构的封装
,序列化的结果
即为 包裹
,而 反序列化操作
则可以理解为 对包裹的拆解
。
序列化操作将数据结构转换为可以 存储或传输的格式的过程(包裹最基本的功能)
,其主要目的是 将数据转化为一种通用的格式(虽然快递中的内容千变万化,但你总不能光看快递的外观就知道里面装着什么)
,以便在不同的环境中使用(世界上可能存在没见过快递的人,但破坏总是人的天赋)
😈。
序列化操作的具体优点存在如下几点:
项目 | 描述 |
---|---|
数据持久化 | 在许多应用程序中,需要将数据结构 保存到磁盘或数据库 中,以便在 应用程序关闭并重新启动时能够恢复该数据结构 。通过序列化,可以将各种数据格式以文本的形式保存到磁盘上,然后在需要时 将文本反序列化以得到原数据格式 。 |
数据传输 | 在网络通信或进程间通信中,需要将数据从一个地方传输到另一个地方。序列化操作能够将各种数据格式转换为可以在网络上传输的格式(普通文本) 。 |
跨平台兼容性 | 不同编程语言和不同操作系统之间 可能使用不同的数据表示方法 。通过序列化操作,可以将数据转换为一种 中立的格式 ,能有效提高不同平台的 互操作性 。 |
远程过程调用 | 远程过程 是服务器上的一个 函数或方法 ,可以由客户端远程调用。客户端通过 RPC 请求 指定要调用的远程过程,并传递所需的参数。服务器接收请求后执行相应的远程过程,并返回执行结果。 |
在 PHP 中,将各种数据结构序列化为文本需要使用到的函数是 serialize()
,该函数接收某一PHP 数据结构的数据作为参数并 返回序列化文本
。
举个栗子
# 定义 PHP 中的对象
class MyClass
{
# 定义对象的属性
public $Name = 'RedHeart';
public $Nation = '中国';
# 定义对象所使用到的方法
public function sayHello(){
print('Hello World');
}
}
# 将 MyClass 类实例化为对象
$myClass = new MyClass();
# 将 myClass 对象转换为序列化文本
$result = serialize($myClass);
# 将序列化结果输出至终端或页面中
var_dump($result);
执行效果
string(70) "O:7:"MyClass":2:{s:4:"Name";s:8:"RedHeart";s:6:"Nation";s:6:"中国";}"
序列化文本本质上是 PHP 中字符串数据结构的具化
,但这并不意味着 字符串
不能被序列化,也不意味着字符串的序列化结果将与原数据保持一致。对此,请参考如下内容:
# 定义数据结构为字符串的 PHP 变量
$string = 'Hello World';
# 将字符串转换为序列化文本
$result = serialize($string);
# 输出被序列化字符串
print('【被序列化字符串】' . $string . "\n");
# 输出序列化结果
print('【序列化结果】' . $result);
执行效果
在 PHP 中,你可以将序列化文本与字符串 理解为两种不同的数据结构
,虽然两者 实际上
都是字符串数据结构的具化。
【被序列化字符串】Hello World
【序列化结果】s:11:"Hello World";
在 PHP 中,对序列化文本执行反序列化操作以使其恢复为原数据格式需要使用到 unserialize()
函数,该函数接收一段序列化文本并 返回反序列化结果(序列化文本的原数据格式)
。对此,请参考如下示例:
# 定义一个数据结构为数组的 PHP 变量
$target = [1, 2, 3];
var_dump($target);
# 序列化操作
$result = serialize($target);
var_dump($result);
# 对序列化文本执行反序列化操作
$un_result = unserialize($result);
var_dump($un_result);
执行效果
array(3) {
[0]=>
int(1)
[1]=>
int(2)
[2]=>
int(3)
}
string(30) "a:3:{i:0;i:1;i:1;i:2;i:2;i:3;}"
array(3) {
[0]=>
int(1)
[1]=>
int(2)
[2]=>
int(3)
}
注:
PHP 对 各种数据结构
都定义了 独特的序列化文本
,对不同格式的序列化文本,PHP 将执行不同的反序列化操作以将其复原为正确的数据格式。
PHP 反序列化操作不会将 对象的方法
转换为序列化文本的一部分,这是 PHP 设计者 出于安全考虑
而做出的决策,该策略旨在降低恶意用户利用反序列化机制对应用造成攻击的可能。对此,请参考如下示例:
class MyClass
{
public $name = 'RedHeart';
public $nation = 'China';
public function sayHello(){
print('Hello World' . "\n");
}
}
var_dump(serialize(new MyClass()));
执行效果
观察序列化文本,可以发现其中不存在任何与方法 sayHello()
相关的字眼。
string(69) "O:7:"MyClass":2:{s:4:"name";s:8:"RedHeart";s:6:"nation";s:5:"China";}"
在对 PHP 进行序列化操作时,仅
会将对象的 属性
转化为序列化文本。而在将序列化文本转化为原数据结构时,将依据序列化文本构造对象的属性,而 该对象的方法将依据当前 PHP 应用中相关的类定义决定
。对此,请参考如下示例:
# 定义类 MyClass
class MyClass
{
public $name;
public $nation;
# 定义方法 sayHello
public function sayHello(){
print('Hello World' . "\n");
}
}
# 序列化文本
$serialize_text = 'O:7:"MyClass":2:{s:4:"name";s:8:"RedHeart";s:6:"nation";s:5:"China";}';
# 对序列化文本执行反序列化操作
$myClass_1 = unserialize($serialize_text);
# 通过 get_class_method() 函数获取对象的方法列表
var_dump(get_class_methods($myClass_1));
执行效果
array(1) {
[0]=>
string(8) "sayHello"
}
作为对比
,我们设计如下 PHP 代码。在该示例中,unserialize
函数的参数与上一个示例相同,但在通过 get_class_method()
函数获取反序列操作得到的结果对象所具有的方法却得到了不同的结果。
# 定义类 MyClass
class MyClass
{
public $name;
public $nation;
# 定义方法 sayBye
public function sayBay(){
print('Bye Bye' . "\n");
}
}
# 相同的序列化文本
$serialize_text = 'O:7:"MyClass":2:{s:4:"name";s:8:"RedHeart";s:6:"nation";s:5:"China";}';
# 对序列化文本执行反序列化操作
$myClass_2 = unserialize($serialize_text);
# 通过 get_class_method() 函数获取对象的方法列表
var_dump(get_class_methods($myClass_2));
执行效果
array(1) {
[0]=>
string(6) "sayBay"
}
在将序列化文本转化为对象时,若此前没有与该对象相关的类定义,反序列化操作过程中,PHP 仍然会尝试重建对象
。为此,PHP 将创建一个 __PHP_Incomplete_Class
对象,其中包含原始类的类名及属性的相关信息
,但 不包含实际的方法和属性
。
$serialize_text = 'O:7:"MyClass":2:{s:4:"name";s:8:"RedHeart";s:6:"nation";s:5:"China";}';
$myClass = unserialize($serialize_text);
var_dump($myClass);
执行效果
object(__PHP_Incomplete_Class)#1 (3) {
["__PHP_Incomplete_Class_Name"]=>
string(7) "MyClass"
["name"]=>
string(8) "RedHeart"
["nation"]=>
string(5) "China"
}
注:
请不要尝试访问 __PHP_Incomplete_Class
对象的任何属性,这将导致 Notice
异常。
在将序列化文本转换为对象时,倘若相关的类定义中存在序列化文本中未出现的属性
,则反序列化产生的结果对象中的属性个数将多余序列化文本中所记录的属性数量,而这些 多出的属性均来自当前反序列操作过程中所依据的相关类定义
。对此,请参考如下示例:
class MyClass
{
public $name = 'RedHeart';
public $nation = 'China';
# 与序列化文本的属性记录存在差异的属性定义
public $sex = 'man';
public $age = 18;
}
$serialize_text = 'O:7:"MyClass":2:{s:4:"name";s:8:"RedHeart";s:6:"nation";s:5:"China";}';
$myClass = unserialize($serialize_text);
var_dump($myClass);
执行效果
object(MyClass)#1 (4) {
["name"]=>
string(8) "RedHeart"
["nation"]=>
string(5) "China"
["sex"]=>
string(3) "man"
["age"]=>
int(18)
}
PHP 的反序列化过程将依据类定义把序列化文本转化为对象。记住,在对象的序列化文本的反序列过程中,类定义才是主角
。反序列过程中,PHP 会依据相关的类定义创建对象并将序列化文本中记录的属性信息实现且附加到该对象之中
。这也就意味着,类定义中声明的属性必然会出现在反序列化的结果对象中
,无论相关的序列化文本中是否存在该属性的相关记录。对此,请参考如下示例:
class MyClass
{
public $name = 'RedHeart';
public $nation = 'China';
public $star;
}
# 序列化文本中记录的属性信息为零
$serialize_text = 'O:7:"MyClass":0{;}';
$myClass = unserialize($serialize_text);
var_dump($myClass);
执行效果
object(MyClass)#1 (3) {
["name"]=>
string(8) "RedHeart"
["nation"]=>
string(5) "China"
["star"]=>
NULL
}