一直以来总是觉得对PHP反序列化漏洞的理解比较模糊,今天抽时间深入学习下PHP反序列化漏洞的成因以及利用方式,在此做一个总结。
参考链接:
http://bobao.360.cn/learning/detail/3193.html
http://blog.csdn.net/qq_32400847/article/details/53873275?readlog
PHP反序列化漏洞,也叫php对象注入(PHP Object Injection)。
序列化与反序列化
说到反序列化,就要提到序列化。序列化和反序列化的目的是使得程序间传输对象更加方便。序列化是将对象转换为字符串以便存储和传输的一种方式。而反序列化就是序列化的逆过程,它会将字符串重新转换为对象供程序使用。
在PHP中序列化和反序列化对应的函数分别为serialize()和unserialize()。反序列化本身并不危险,但是如果反序列化时传入反序列化函数的参数是用户可控的,反序列化漏洞就产生了。
PHP手册中关于对象序列化的描述:
所有php里面的值都可以使用函数serialize()来返回一个包含字节流的字符串来表示,unserialize()函数能够重新把字符串变回php原来的值。反序列化一个对象将会保存对象的所有变量。但是不会保存对象的方法,只会保存类的名字。 为了能够unserialize()一个对象,这个对象的类必须已经定义过。如果序列化类A的一个对象,将会返回一个跟类A相关,而且包含了对象所有变量值的字符串。如果要想在另外一个文件中解序列化一个对象,这个对象的类必须在解序列化之前定义,可以通过包含一个该类的文件或使用函数spl_autoload_register()来实现。
PHP中的magic方法(函数):
魔术方法就是在某些条件下自动执行的函数。
为什么会有魔术方法:
魔术方法是在需要实现一些功能,但是一般代码做不到或很难做到的时候才能用。在命名自己的类方法时不能使用这些方法名,除非是想使用其魔术功能。
php类可能会包含一些特殊的函数叫magic方法,magic方法命名是以符号开头的,比如construct()、destruct()、toString()、sleep()、wakeup()等等,这些函数在某些情况下会自动调用。一些magic函数及其用途如下:
__construct() 当一个对象创建时触发
__destruct() 当一个对象被销毁时触发
__toString() 把类当作字符串使用时触发
__call() 在对象上下文中调用不可访问的方法时触发
__callStatic() 在静态上下文中调用不可访问的方法时触发
__get() 用于从不可访问的属性读取数据时
__set() 用于将数据写入不可访问的属性
__wakeup() 使用unserialize时触发
__sleep() 使用serialize时触发
__isset() 在不可访问的属性上调用isset()或empty()触发
__unset() 在不可访问的属性上使用unset()时触发
__invoke() 当脚本尝试将对象调用为函数时触发
__autoload() 尝试加载未定义的类时触发
__clone() 当对象复制完成时触发
配合代码讲解,更容易理解魔术方法怎么调用的:
<?php
class Brucetg{
public $test = "This is just a test";
public function PrintTest()
{
echo $this->test.'<br/>';
}
public function __construct()
{
echo '__construct</br>';
}
public function __destruct()
{
echo '__destruct<br />';
}
public function __toString()
{
return '__toString<br />';
}
}
$object = new Brucetg();
$object->PrintTest();
echo $object;
?>
为什么会有序列化和反序列化:
php允许保存一个对象方便以后使用,这个过程被称为序列化。为什么要有序列化这种机制呢?在传递变量的过程中,有可能遇到变量值要跨脚本文件传递的过程。试想,如果另一个脚本想要调用之前一个脚本的变量,但是前一个脚本已经执行完毕,所有的变量和内容已经释放掉了,我们要如何操作呢?难道要一个脚本不断地循环,等待后面脚本调用?这肯定是不现实的。serialize和unserialize就是用来解决这一问题的。serialize可以将变量转换为字符串并且在转换中可以保存当前变量的值;unserialize则可以将serialize生成的字符串转换为变量。
// 1.php
<?php
class User
{
public $name = '';
public $age = 0;
public $sex = '';
public function PrintUser()
{
echo 'User '.$this->name.' is ' . $this->age .' years old, sex is '.$this->sex.'.<br/>';
}
}
//创建一个对象
$user = new User();
$user->name = 'Test';
$user->age = 20;
$user->sex = 'male';
$user->PrintUser();
echo serialize($user);
?>
为了使用这个对象,在2.php中用unserialize重建对象。
//2.php
<?php
class User{
public $age = 0;
public $name = '';
public $sex = '';
public function PrintUser()
{
echo 'User ' .$this->name.' is '.$this->age.' years old, sex is '.$this->sex.'. ';
}
}
$user = unserialize('O:4:"User":3:{s:4:"name";s:4:"Test";s:3:"age";i:20;s:3:"sex";s:4:"male";}');
$user->PrintUser();
?>
反序列化漏洞的利用思路:
在反序列化中,我们所能控制的数据就是对象中的各个属性值,所以在PHP的反序列化有一种漏洞利用方法叫做“面向属性编程”,即POP(Property Oriented Programming)。和二进制漏洞中常用的ROP技术类似。在ROP技术中我们往往需要一段初始化gadgets来开始我们整个的利用过程,然后继续调用其他的gadgets。在PHP反序列化漏洞利用技术POP中,对应的初始化gadgets就是wakeup()或者是destruct()方法。在最理想的情况下能够实现漏洞利用的点就在这两个函数中,但往往我们需要从这个函数开始,逐步的跟进在这个函数中调用到的所有函数,直到找出可以利用的点为止。以下为跟进其函数调用过程中需要关注的一些很有价值的函数:
Command Execution:
exec()
passthru()
popen()
system()
File Access:
file_put_contents()
file_get_contents()
unlink()
当然并不只有这些函数才会导致漏洞的产生,比如前几天typecho反序列化漏洞最终利用的是call_user_function(),漏洞分析中也提到了array_map()函数,该函数在传递给它的参数是用户可控时也会导致漏洞的产生。
POP: 简单来说就是关注整个函数的调用过程中参数的传递情况,找到可利用的点,这和一般的Web漏洞没什么区别,只是可控制的值由直接传递给程序的参数转变为了对象中的值。
反序列化漏洞挖掘:
PHP的unserialize()函数只能反序列化在当前程序上下文中已经被定义过的类。在传统的PHP中你需要通过使用一大串的include()或者require()来包含所需的类定义文件。于是后来出现了autoloading技术,它可以自动导入需要使用的类。这种技术同时也方便了我们的漏洞利用。因为在我们找到一个反序列化点的时候我们所有使用的类就多了,那么实现漏洞利用的可能性也就更高。
还有一个东西要提一下,那就是Composer,这是一个php的包管理工具,同时他还能自动导入所依赖库中定义的类,这样一来unserialize()函数也就能使用所有依赖库中的类了,攻击面又增大不少。