为什么要序列化
百度百科上关于序列化的定义是,将对象的状态信息转换为可以存储或传输的形式(字符串)的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区(非关系型键值对形式的数据库Redis,与数组类似)。以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。
在PHP中,每个类的定义都以关键字class开头,后面不仅跟着类名还跟着一对花括号,里面包含有类的属性与方法的定义。
一个类可以包含有属于自己的属性(常量:值变量的值不会改变、变量)和方法(函数)。
由于类的实例化对象比较抽象,不方便用于传输和存储。
tips:类为class,对象是object;举例来说就是,动物为一个大类,大类之后再分小一类如猫猫或者狗狗;而具体的某个动物就为一个对象。一个对象就相当于一个变量,对象是一种语言结构。
简单来说,序列化就是把一个对象变成可以传输的字符串(字符串便于传输,只需要把字符串赋给变量即可)。在传递和保存对象时,为保证对象的完整性和可传递性,程序将对象转换为有序字节流,以保存在本地文件中,可以以特定的格式在进程之间跨平台、安全的进行通信。(比如从java平台传递到php平台)反序列化则根据字符串中保存的对象状态及描述信息,通过反序列化重建对象。
序列化的优点:
将对象转为字节流存储到硬盘上(实际上是存放在数据库,一般是redis数据库-键值对数据库),当JVM停机的话,字节流还会在硬盘上默默等待,等待下一次JVM的启动,把序列化的对象,通过反序列化为原来的对象。
序列化后的二进制序列能够减少存储空间(永久性保存对象)。
序列化成字节流形式的对象可以进行网络传输。
通过序列化可以在进程间传递对象
实际上用Redis数据库作为缓存,一般用于存储序列化后的字符串,待字符串需要使用时,再反序列化为对象,方便调用。
PHP中的序列化与反序列化
反序列化漏洞并不是PHP特有,也存在于Java、Python等语言之中,但其原理基本相通。
序列化与反序列化的定义
序列化:程序将对象状态转换为可存储或传输的字节序列的过程(即对象状态转换为可存储或者可传输的过程){序列化是对象转换为字符串的过程}
反序列化:程序把存储或传输的字节序列恢复为对象的过程。{反序列化则是字符串转化为对象的过程}
如果字符串客户端可控,就会造成web应用反序列化任意对象,严重的是在反序列化的过程中会触发一些可以执行的php代码,例如phpinfo
PHP中的序列化与反序列化,基本都是围绕serialize()和unserialize()两个函数展开的。在介绍这两个函数之前,我们可以先看一个简单的例子。
简单的例子
我们可以用json格式数据的编码与解码(json格式就是格式化的字符串,也就是说json格式的数据,它具备一定的格式),来理解序列化与反序列化的过程。虽然json数据与反序列化漏洞没有什么关系,但是这个例子有助于我们理解。代码内容如下:
<?php
$student=array('name'=>'AJEST','age'=>18,'SEX=>true','score'=>89.9);
// echo不能直接输出数组,但是如果把$student给做一个json_encode 的格式转换,就可以用echo来输出
echo $student;
echo "<hr/>";
$student_json=json_encode($student);
echo $student_json;
?>
我们定义一个数组,数组属于抽象的数据结构,为了方便跨平台传输数据,可以将其进行json编码。json格式的数据是以键值对的形式出现的。类似于这种格式的数据易于处理易于传输。结果如下
序列化与反序列化Demo
序列化会将一个抽象的对象转换为字符串。
我们可以写一个Demo来说明序列化的过程,首先创建一个类,代码内容如下:
<?php
class student{
public $name;
public $sex;
public $age;
public $score;
}
?>
类名是student,该类中有四个变量。接下来,我们可以将这个类实例化,也就是创建一个对象(new),并给对象中变量赋值。代码如下:
<?php
// 定义一个student类,类中有四个属性
class student{
public $name;
public $sex;
public $age;
public $score;
}
// 创建对象1
$student1 = new student();
$student1->name = "wyy";
$student1->sex = false;
$student1->age = 20;
$student1->score = 95;
?>
最后我们使用serialize(),将$student1这个对象序列化成一个字符串。这样的字符串就很容易传输和存储了。如下:
<?php
// 定义一个student类,类中有四个属性
class student{
public $name;
public $sex;
public $age;
public $score;
}
// 创建对象1
$student1 = new student();
$student1->name = "wyy";
$student1->sex = false;
$student1->age = 20;
$student1->score = 95;
// 创建对象2
$student2 = new student();
$student2->name = "xcc";
$student2->sex = true;
$student2->age = 25;
$student2->score = 98;
// 输出wyy和xcc的成绩
echo $student1->name."'s score = ".$student1->score;
// 进行换行
echo "<br/>";
echo $student2->name."'s score = ".$student2->score;
// 用var_dump输出对象
echo "<hr/>";
var_dump($student1);
var_dump($student2);
// 对对象进行序列化并输出
echo "<hr>";
echo "序列化后采用echo输出<br>";
echo serialize($student1);
?>
打开浏览器访问该文件,显示如下,可以看到对象被序列化成字符串:
同样,我们可以使用unserialize()函数,将字符串反序列化成为一个对象。由于字符串中含有双引号,所以此处可以使用定界符的方法定义字符串。代码如下:
<?php
// 定义一个student类,类中有四个属性
class student{
public $name;
public $sex;
public $age;
public $score;
}
// 创建对象1
$student1 = new student();
$student1->name = "wyy";
$student1->sex = false;
$student1->age = 20;
$student1->score = 95;
// 创建对象2
$student2 = new student();
$student2->name = "xcc";
$student2->sex = true;
$student2->age = 25;
$student2->score = 98;
// 输出wyy和xcc的成绩
echo $student1->name."'s score = ".$student1->score;
// 进行换行
echo "<br/>";
echo $student2->name."'s score = ".$student2->score;
// 用var_dump输出对象
echo "<hr/>";
var_dump($student1);
var_dump($student2);
// 对对象进行序列化并输出
echo "<hr>";
echo "<br>";
// 序列化后采用echo输出
echo serialize($student1);
$str =<<<HTML
O:7:"student":4:{s:4:"name";s:3:"wyy";s:3:"sex";b:0;s:3:"age";i:20;s:5:"score";i:95;}
HTML;
echo "<hr/>";
var_dump(unserialize($str));
?>
综上,序列化→我们将对象序列化成字符串,反序列化→我们将字符串反序列化成对象
运行这个脚本文件,我们可以看到反序列化后的对象
PHP反序列化漏洞
PHP反序列化漏洞也叫php对象注入,是一个非常常见的漏洞,这种类型的漏洞虽然有些难以利用,但一旦利用成功就会造成非常危险的后果。
漏洞的形成的根本原因是程序没有对用户输入的反序列化字符串进行检测,导致反序列化过程可以被恶意控制,进而造成代码执行、getshell等一系列不可控的后果。
PHP中的序列化与反序列化,基本都是围绕serialize()和unserialize()两个函数展开的。
实验步骤
实验一
在网站根目录新建Code文件夹,在Code文件夹中新建PHP文件夹,再从PHP文件夹中新建Class文件夹,最后在Class文件夹下新建文件unserialize.php,文件内容如下:
<?php
// 定义一个test类,类中有4个属性,未定义方法
class Test{
public $str='AJEST;';
function _destruct(){
// echo "This is function_construct()";
@eval($this->str);
}
}
// 创建对象并序列化后输出
$test =new Test();
echo serialize($test);
echo "<hr/>";
$t = serialize($test);
var_dump(unserialize($t));
?>
打开浏览器访问该文件,显示如下
如下修改文件内容
<meta charset = "utf-8">
<?php
//定义一个stu类,类中有4个属性,暂未定义方法。
class test{
public $str='AJEST';
function __destruct(){
// 在销毁对象时自动调用
echo "This is function __destruct()";
@eval($this->str);
}
}
//创建对象并序列化后输出
$test = new test();
echo "序列化后采用echo输出:<br>";
echo serialize($test1);
echo "<hr>";
//输入序列化后的字符串,并采用凡序列化后采用var_dump输入
$obj=$_GET['obj'];
echo "反序列化后采用var_dump输出:<br>";
var_dump(unserialize($obj));
?>
继续打开浏览器访问该文件,输入参数?obj=?obj=O:4:"test":1:{s:3:"str";s:5:"hello";},网页内容显示如下,含有两句“ This is function __destruct()”,说明函数被调用两次。注意,当销毁实例化类(对象)的时候,__destruct()函数会被自动调用
接下来修改参数?obj=O:4:"test":1:{s:3:"str";s:10:"phpinfo();";},网页显示如下,可以看到phpinfo()函数被成功执行。
由以上代码,我们会发现,PHP的反序列化漏洞学院与其他漏洞配合,如代码执行SQLi等。
我们输入的phpinfo()为何会作为PHP语句运行呢?
我们观察代码,发现在类中有一个函数_destruct()并且这个函数调用的eval语句,执行$this->str变量。为什么_destruct()没有被调用,函数内的语句就会被执行呢?
我们发现,当销毁实例化类(对象)的时候,_destruct()函数会被自动调用,并输出字符串[This is function_destruct()]。
实验二
将unserialize.php文件修改如下,将new新建test类对象的语句注释掉,再次实验
<meta charset = "utf-8">
<?php
//定义一个stu类,类中有4个属性,暂未定义方法。
class test{
public $str='AJEST';
function __destruct(){
echo "This is function __destruct()";
@eval($this->str);
}
}
//创建对象并序列化后输出
//$test1 = new test();
//echo "序列化后采用echo输出:<br>";
//echo serialize($test1);
echo "<hr>";
//输入序列化后的字符串,并采用凡序列化后采用var_dump输入
$obj=$_GET['obj'];
echo "反序列化后采用var_dump输出:<br>";
var_dump(unserialize($obj));
在浏览器中输入参数?obj=?obj=O:4:"test":1:{s:3:"str";s:5:"hello";},网页显示如下,含有1句"This is function_destruct()",说明函数被调用1次,注意,当销毁实例化类(对象)的时候,__destruct()函数会被自动调用
继续在浏览器中输入?obj=O:4:"test":1:{s:3:"str";s:10:"phpinfo();";},网页显示如下,可以看到同样能执行phpinfo()函数,因此可以判断输入对象成功反序列化为test类对象,并自动执行了__destruct()函数。
实验三
unserialize.php文件修改将最后一行语句修改如下,实验如下
<meta charset = "utf-8">
<?php
//定义一个stu类,类中有4个属性,暂未定义方法。
class test{
public $str='AJEST';
function __destruct(){
echo "This is function __destruct()";
@eval($this->str);
}
}
//创建对象并序列化后输出
//$test1 = new test();
//echo "序列化后采用echo输出:<br>";
//echo serialize($test1);
echo "<hr>";
//输入序列化后的字符串,并采用凡序列化后采用var_dump输入
$obj=$_GET['obj'];
//echo "反序列化后采用var_dump输出:<br>";
//var_dump(unserialize($obj));
$obj1=unserialize($obj);
?>
浏览器同样输入参数?obj=O:4:"test":1:{s:3:"str";s:5:"hello";},网页显示如下,含有1句“ This is function __destruct()”,说明函数被调用1次。
浏览器同样输入参数?obj=O:4:"test":1:{s:3:"str";s:10:"phpinfo();";}访问,,网页显示如下,可以看到同样能执行phpinfo()函数,因此可以判断输入对象成功反序列化为test类对象,并自动执行了__destruct()函数。
tips:
我们发现,当销毁实例化类的时候,__destruct()函数会被自动调用,并输出字符串This is function__destruct()。
默认情况下,PHP仅仅释放对象属性所占用的内存并销毁对象相关的资源,__destruct()允许你在使用一个对象之后执行任意代码来清除内存,当PHP决定你的脚本不再与对象相关时,__destruct()将被调用.,在一个函数的命名空间内,这会发生在函数return的时候,对于全局变量,这发生于脚本结束的时候,如果你想明确地销毁一个对象,你可以给指向该对象的变量分配任何其它值,通常将变量赋值勤为NULL或者调用unset。
PHP魔术方法归纳
以[_]开头的方法,是PHP中的魔术方法,类中的魔术方法,在特定情况下会被自动调用。主要魔术方法及其触发条件如下。
魔术方法 | 触发条件 |
_construct() | 构造方法,当一个对象被创建时调用 |
_destruct() | 析构方法,PHP将在对象被销毁前(即从内存中清除前)调用 |
_autoload() | 使用尚未被定义的类时自动调用。通过此函数,脚本引擎在PHP出错失败前有了最后一个机会加载所需的类。 |
_call($method,$arg_array) | 在对象中调用一个不可访问方法时 |
_callStatic() | 在静态上下文中调用一个不可访问的方法时使用 |
_clone() | 使用clone方法复制一个对象时 |
_invoke() | 当尝试调用函数的方式调用一个对象时,_invoke方法会被自动调用 |
_get($property) | 从不可访问的属性中读取数据 |
_set($property,$value) | 给一个未定义的属性赋值时调用 |
_isset($property) | 在一个未定义的属性上调用isset()函数时调用此方法 |
_unset($property) | 在一个未定义的属性上调用unset()函数时调用此方法 |
_toString() | 在将一个对象转化成字符串时自动调用 |
_sleep() | 序列化对象前调用(其返回需要是一个数组) |
_wakeup() | 反序列化恢复对象前调用 |
_set_state() | 当调用var_export()时,这个静态方法就会被调用 |
注意:
serialize() 检查类中是否有魔术名称 __sleep 的函数。如果这样,该函数将在任何序列化之前运行。它可以清除对象并应该返回一个包含有该对象中应被序列化的所有变量名的数组。
使用 __sleep 的目的是关闭对象可能具有的任何数据库连接,提交等待中的数据或进行类似的清除任务。此外,如果有非常大的对象而并不需要完全储存下来时此函数也很有用。
相反地,unserialize() 检查具有魔术名称 __wakeup 的函数的存在。如果存在,此函数可以重建对象可能具有的任何资源。
使用 __wakeup 的目的是重建在序列化中可能丢失的任何数据库连接以及处理其它重新初始化的任务。