在了解PHP序列化和反序列化,之前需要对PHP的类与对象、魔法方法等有所了解,此外PHP对象注入漏洞,作为一种极为
常见却很难被利用的漏洞,也是由于PHP的序列化与反序列化早成的
不论在哪种编程语言中,类与对象的概念都不会被绕过,它是面向对象编程的基础知识,也是必备知识。
class animal {
public $name = 'dahuang';//define a virable
public function sleep(){//define a simpe method
echo $this->name . " is sleeping...\n";
}
}
$dog = new animal();
$dog->sleep();
?>
上述简单的代码中,定义了一个animal类,在animal类中定义了一个$ name变量和一个sleep方法;然后对animal类实例化,创建一个dog对象,通果dog对象调用sleep方法,输出。
PHP类中一般会包含一些特殊的函数叫做magic函数,这些函数以双斜划线开始,他们的主要作用是在某些情况下,自动调用,以保证程序的健壮性,但也是由于这些方法的自动调用,使得程序在一些情况下存在漏洞。
PHP常见的magic方法:
方法名 | 触发点 |
---|---|
__construct | 在创建对象时候初始化对象,一般用于对变量赋初值 |
__destruct | 和构造函数相反,在对象不再被使用时(将所有该对象的引用设为null)或者程序退出时自动调用 |
__toString | 当一个对象被当作一个字符串被调用,把类当作字符串使用时触发,返回值需要为字符串 |
__wakeup() | 使用unserialize时触发,反序列化恢复对象之前调用该方法 |
__sleep() | 使用serialize时触发 ,在对象被序列化前自动调用,该函数需要返回以类成员变量名作为元素的数组(该数组里的元素会影响类成员变量是否被序列化。只有出现在该数组元素里的类成员变量才会被序列化) |
__destruct() | 对象被销毁时触发 |
__call() | 在对象上下文中调用不可访问的方法时触发,即当调用对象中不存在的方法会自动调用该方法 |
__callStatic() | 在静态上下文中调用不可访问的方法时触发 |
__get() | 用于从不可访问的属性读取数据,即在调用私有属性的时候会自动执行 |
__set() | 用于将数据写入不可访问的属性 |
__isset() | 在不可访问的属性上调用isset()或empty()触发 |
__unset() | 在不可访问的属性上使用unset()时触发 |
__invoke() | 当脚本尝试将对象调用为函数时触发 |
实例:
class animal {
public $name = 'dahuang';//define a virable
public function sleep(){//define a simpe method
echo $this->name . " is sleeping...\n";
}
public function __construct(){
echo "the method:__construct is called\n";
}
public function __destruct(){
echo "the method:__destruct is called\n";
}
public function __toString(){
return "the method:__toString is called\n";
}
}
$dog = new animal();
$dog->sleep();
echo $dog;
?>
1、当通过new对类进行实例化时,首先调用__construct()构析函数,创建dog对象;
2、让后通过dog对象调用sleep()方法,触发sleep,在进行$dog->sleep();时, $ dog备档最字符串,__toString()被调用;
3、echo $dog使得脚本结束,调用__destruct函数。
在PHP网站中的定义:
所有PHP里面的值都是可以使用函数serialize()来返回一个包含字节流的字符串表示。 unserialize()函数能够重新把字符串
变回PHP原来的值。序列化一个对象将会保存对象的所有变量,但不会保存对象的方法,只会保存类的名字。
简单的理解序列化:就是把一个类的实例变成一个字符串;
简单的理解反序列化:就是把一个特殊的字符串转换成一个实例。
**那么为什么要有序列化这种机制存在呢?或者说序列化有什么作用呢?**
因为在传递变量的过程中,有可能遇到变量值要跨脚本文件传递的过程。试想,如果一个脚
本想要调用之前一个脚本的变量,但是前一个脚本已经执行完毕,所有的变量和内容都已经
释放掉了,我们要如何操作,才能引用前一个脚本的变量呢?难道要前一个脚本不断的循环
等待后面脚本的调用,显然这是不现实的。serialize和unserialize(即序列化和反序列
化)就是用来解决这一问题的。serialize可以将变量转换为字符串并且在变换中可以保存当
前变量的值;unserialize则可以将serialize生成的字符串转换为变量。
现在,我们将0x02中的dog对象进行序列化,即将最后一行代码:echo $dog;改
为echo serialize( $dog);
输出结果:高亮部分即为序列化的dog对象
O:6:"animal":1:{s:4:"name";s:7:"dahuang";}
对象类型:长度:"名字":类中变量的个数:{类型:长度:"名字";类型:长度:"值";......}
序列化格式中的字母含义:
a - array b - boolean
d - double i - integer
o - common object r - reference
s - string C - custom object
O - class N - null
R - pointer reference U - unicode string
下面,我们再来探究一个小问题,到我们的变量受到不同修饰符修饰时,会不会有
不同的结果,以animal类中的name变量为例:
1.当name受到public修饰时:private $name = ‘dahuang’;
2.当name受到private修饰时:private $name = ‘dahuang’;
类名加上变量名占12个字节,比正常多了两个字节
3.当name受到protected修饰时:protected $name = ‘dahuang’;
*name占用七个字节
通过对比发现,在受保护的成员前都多了两个字节,受保护的成员在序列化时规则:
其中,"\00"代表ASCII为0的值,即空字节," * " 必不可少。
在说明PHP对象反序列化之前,我们来看一下,在PHP的序列化与反序列化过程中
__sleep()、__wakeup()的调用过程,还是以上面的animal类为例:
对上面的代码进行简单修改
class animal {
public $name = 'dahuang';//define a virable
public $age = '20';
public function eat(){//define a simpe method
echo $this->name . " is eatting...\n";
}
public function __construct(){
echo "the method:__construct is called\n";
}
public function __destruct(){
echo "the method:__destruct is called\n";
}
public function __toString(){
return "the method:__toString is called\n";
}
public function __wakeup(){
echo "the method:__wakeup is called\n";
}
public function __sleep(){
echo "the method:__sleep is called\n";
return array('name','age');
}
}
$dog = new animal();//对类进行实例化时,自动调用__construct()
echo serialize($dog)."\n";
$serializedDog = serialize($dog);//对dog对象进行序列化时,自动调用__sleep()
echo $serializedDog . "\n";//echo 序列化的dog对象
$dog->eat();//dog对象调用eat()方法
//程序结束,调用__destruct()
?>
输出结果:
PHP的几个魔法函数,在进行序列化的过程中的调用:
1、当不进行序列化时: 在进行类的实例化时,自动调用__construct();在输出对象时,自动调用
__toString();在程序结束时,自动调用__destruct();__sleep()与__wakeup()均与序列化与反序列化
有关,在此过程不被调用。
2、当进行序列化时: 在进行类的实例化时,自动调用__construct();在对创建的dog对象进行序列化
时,自动调用__sleep();echo $serializedDog,输出序列化的dog对象,**在此不再调用
_toString()**;dog兑现调用eat()方法,然后程序结束,调用__destruct().
3、在整个过程中,__construct()总是在程序的开始调用,__destruct()总是在程序的结束调用,这很
简单,因为,对所有的变量的初始化总是在程序的开始,释放变量总是在程序结束。
现在,在上面的基础上,对序列化的dog对象进行反序列化:
修改代码、如下:
class animal {
public $name = 'dahuang';//define a virable
public $age = '20';
public function eat(){//define a simpe method
echo $this->name . " is eatting...\n";
}
public function __construct(){
echo "the method:__construct is called\n";
}
public function __destruct(){
echo "the method:__destruct is called\n";
}
public function __toString(){
return "the method:__toString is called\n";
}
public function __wakeup(){
echo "the method:__wakeup is called\n";
}
public function __sleep(){
echo "the method:__sleep is called\n";
return array('name','age');
}
}
$dog = new animal();//对类进行实例化时,自动调用__construct()
$serializedDog = serialize($dog);//对dog对象进行序列化时,自动调用__sleep()
echo $serializedDog . "\n";//echo 序列化的dog对象
$newDog = unserialize($serializedDog);//反序列化已经被序列化的dog对象,自动调用__wakeup()
var_dump($newDog);//输出反序列化的结果
$newDog->eat();//dog对象调用eat()方法
//程序结束,调用__destruct()
?>
分析一下,在进行序列化与反序列化的过程中,几个魔法函数的调用过程:
首先,在对animal类进行实例化时,自动调用__construct()对变量进行初始化;然后通过serialize()方
法对dog对象进行序列化,自动调用__sleep();输出序列化的dog对象;之后,通过unserialize()方法
对序列化的dog对象进行反序列化,自动调用__wakeup()方法;输出反序列化的结果,可以看到被序
列化的dog对象得到了还原;然后通过新的newDog对象调用eat()方法,程序结束,自动调用
__destruct()方法,注意这里的__construct()被调用了两次,这是因为在整个过程中产生了两个对象
dog和newDog,在程序结束需要分写释放。
我们已经认识到了PHP的序列化与反序列化过程,但是如何利用这些漏洞呢?这取决与应用程序、可
用的类和magic方法,序列化对象包含攻击者控制的对象值。在利用反序列化漏洞时,第一步就
是在web应用程序寻找定义的__wakeup()和__sleep()方法,通过上面的讲述可以知道这两个magic方
法只有在进行序列化和反序列化时才会被调用,通过触发这些方法,利用反序列化漏洞。
例如,我们找到一个类用于临时将日志存储在某个文件,当__destruct()被调用时,删除日志文件。
saveLog.php:
class logFile{
public $logname = "error.log";//define the name of log
public function saveLog($text){//define the method to write the contents to a logfile
echo "Log same data: " . $text . "
";
file_put_contents($this->logname, $text . PHP_EOL, FILE_APPEND);
}
public function __destruct(){//delete the logfile when __destruct is called
echo "the method:__destruct is called
";
unlink(dirname(__FILE__) . DIRECTORY_SEPARATOR . $this->logname);//delete the logfile
if(!file_exists(dirname(__FILE__) . DIRECTORY_SEPARATOR . $this->logname)){//if the file has been deleted ,echo the log
echo "the method:__destruct is called and " . $this->logname ." has been deleted
";
}
}
}
deleteLog.php
include 'saveLog.php'; //import files
$obj = new logFile(); //create a new object
$obj->logname = 'somefile.log';//assignment the variable
$obj->saveLog('this is a test file');//call the method to save some contents
首先,注释掉saveLog.php中的unlink()方法看,是否能保存,将两个文件上传至服务器,访问
deletLog.php:可以看到由于unlink()被注释掉了,保存的日志文件并没有被删除;但是输出“the method:__destruct is called”,__destruct()方法已经调用
去掉注释看一下,真正的效果:在deleteLog.php中的__destruct()添加了判断函数,所以当文件被删除后弹出日志;文件被删除。
对1中的saveLog.php进行优化:
saveLog.php:
class logFile{
public $logname = "error.log";//define the name of log
public function saveLog($text){//define the method to write the contents to a logfile
echo "Log same data: " . $text . "
";
file_put_contents($this->logname, $text . PHP_EOL, FILE_APPEND);
}
public function __destruct(){//delete the logfile when __destruct is called
echo "the method:__destruct is called
";
if(!file_exists(dirname(__FILE__) . DIRECTORY_SEPARATOR . $this->logname)){//if the file has been deleted ,echo the log
echo "the method:__destruct is called and " . $this->logname ." has been deleted
";
}else{
unlink(dirname(__FILE__) . DIRECTORY_SEPARATOR . $this->logname);//delete the logfile
}
}
}
创建测试文件testPHP.php:
echo "this is a test file";
?>
创建序列化脚本serialize.php
include 'saveLog.php';
$obj = new logFile();
$obj->logname = "testPHP.php";
echo serialize($obj)."
";
创建反序列化脚本unserialize.php
include 'saveLog.php';
$usr = unserialize($_GET['user_serialized']);//用户可控
var_dump($usr);
echo "
";
测试:
1.让问testPHP.php进行测试:
2.访问serialize.php:
获得序列化的字符串:O:7:“logFile”:1:{s:7:“logname”;s:11:“testPHP.php”;}
并且原来的测试文件testPHP.php已经通过自动调用__destruct()被删除。
3.访问testPHP.php:文件已经在访问serialize.php时被删除;
4.通过反序列化恢复字符串,访问:恢复数据结构
http://192.168.15.83/unserialize.php/?user_serialized=O:7:"logFile":1:{s:7:"logname";s:11:"testPHP.php";}
此时,testPHP.php文件已经被删除,因为在脚本运行结束时,__destruct()方法,然而我们可以通过
给定不同的字符串控制logFile类的变量。这就是PHP反序列化漏洞的名称由来,PHP反序列化漏洞的
条件:
1.unserialize函数的变量可控(本例中:unserialize($_GET[‘USER_serialized’]):USER_serialized是用户设定);
2.php文件中存在可利用的类,类中有魔术方法(本例中:__destruct());
对0x04中代码进行修改:
class animal {
public $name = 'dahuang';//define a virable
public $age = '20';
public function eat(){//define a simpe method
echo $this->name . " is eatting...
";
}
public function __construct(){
echo "the method:__construct is called
";
}
public function __destruct(){
echo "the method:__destruct is called
";
}
public function __toString(){
return "the method:__toString is called
";
}
public function __wakeup(){
echo "the method:__wakeup is called
";
}
public function __sleep(){
echo "the method:__sleep is called
";
return array('name','age');
}
}
$usr = unserialize($_GET['USER_serialized']);
$usr->eat();
var_dump($usr);//echo the result
echo "
";
?>
输出结果:
payload:http://192.168.15.83/test1.php/?USER_serialized=O:6:"animal":2:{s:4:"name";s:7:"dahuang";s:3:"age";s:2:"20";}
1.创建测试文件demo.txt
2.重新编写类脚本:logfile.php
class LogFile
{
public $filename = 'error.log'; //define the name of log
public function LogData($text) //define a method to save log
{
echo 'Log some data: ' . $text . '
';
file_put_contents($this->filename, $text, FILE_APPEND);
}
public function __destruct() //define a method to delete log
{
echo 'the method:__destruct is called and the file :"' . $this->filename . '" is deleteed.
';
unlink(dirname(__FILE__) . '/' . $this->filename);
}
}
?>
3.反序列testunserialize.php
//import files here
class FileClass
{
public $filename = 'error.log'; //the name of file
public function __toString() //when the object read as a string , this method will be called
{
return file_get_contents($this->filename);
}
}
$obj = unserialize($_GET['usr_serialized']);// implement user can control
echo $obj; //echo the __toString
?>
4.测试文件:testfile.php
include 'testunserialize.php';
$fileobj = new FileClass();
$fileobj->filename = '1.txt';
echo serialize($fileobj);
?>
5.测试:
5.1直接访问测试文件demo.txt
5.2 访问测试文件获取序列化字符串:O:9:“FileClass”:1:{s:8:“filename”;s:5:“1.txt”;}
5.3 漏洞利用,访问反序列化测试文件读取文件内容:成功读取文件内容,漏洞利用成功
在上面的实验过程介绍了PHP常见的几个magic函数的调用顺序和调用方法,在进行反序列化漏洞测试的过程中主要利用到了__destruct和__tostring两个magic函数。也可以使用其他magic函数:如果对象将调用一个不存在的函数__call将被调用;如果对象试图访问不存在的类变量__get和__set将被调用。但是利用这种漏洞并不局限于magic函数,在普通的函数上也可以采取相同的思路。例如User类可能定义一个get方法来查找和打印一些用户数据,但是其他类可能定义一个从数据库获取数据的get方法,这从而会导致SQL注入漏洞。set或write方法会将数据写入任意文件,可以利用它获得远程代码执行。唯一的技术问题是注入点可用的类,但是一些框架或脚本具有自动加载的功能。最大的问题在于人:理解应用程序以能够利用这种类型的漏洞,因为它可能需要大量的时间来阅读和理解代码。
1.unserialize函数的变量可控(本例中:unserialize($_GET[‘USER_serialized’]):USER_serialized是用户设定);
2.php文件中存在可利用的类,类中有魔术方法(本例中:__destruct());
1.对参数进行处理;
2.使用更加安全的函数。
参考:
https://paper.seebug.org/680/
https://blog.csdn.net/nzjdsds/article/details/82703639
https://cloud.tencent.com/developer/news/88891
https://www.jianshu.com/p/1d2c65601d2a
http://www.codeceo.com/article/php-object-injection.html
https://blog.csdn.net/fly_hps/article/details/82734823
https://www.freebuf.com/news/172507.html
https://www.freebuf.com/articles/web/167721.html
https://www.cnblogs.com/wfzWebSecuity/p/11156279.html