反序列化漏洞原理

访问控制修饰符

PHP的一个标准的类的定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class TestClass //定义一个类
{
//一个变量
public $variable = 'This is a string';
//一个方法
public function PrintVariable()
{
echo $this->variable;
}
}
//创建一个对象
$object = new TestClass();
//调用一个方法
$object->PrintVariable();
?>

和C语言类似,PHP 对属性或方法的访问控制,是通过在前面添加关键字 public(公有),protected(受保护)或 private(私有)来实现的。

重点来了,PHP中访问控制修饰符不同,序列化后属性的长度和属性值会有所不同

public:属性被序列化的时候属性值会变成 属性名

protected:属性被序列化的时候属性值会变成 \x00*\x00属性名

private:属性被序列化的时候属性值会变成 \x00类名\x00属性名

比如下面的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class People{
public $id;
protected $gender;
private $age;
public function __construct(){
$this->id = 'Hardworking666';
$this->gender = 'male';
$this->age = '18';
}
}
$a = new People();
echo serialize($a);
?>

输出如下

1
O:6:"People":3:{s:2:"id";s:14:"Hardworking666";s:9:" * gender";s:4:"male";s:11:" People age";s:2:"18";}

魔术方法

PHP中把以两个下划线__开头的方法称为魔术方法(Magic methods)

serialize() 函数会检查类中是否存在一个魔术方法。如果存在,该方法会先被调用,然后才执行序列化操作。

我们需要重点关注一下5个魔术方法:

__construct:构造函数,当一个对象创建时调用

__destruct:析构函数,当一个对象被销毁时调用

__toString:当一个对象被当作一个字符串时使用

__sleep:在对象序列化的时候调用

__wakeup:对象重新醒来,即由二进制串重新组成一个对象的时候(在一个对象被反序列化时调用)

从序列化到反序列化这几个函数的执行过程是:

construct() ->sleep() -> wakeup() -> toString() -> __destruct()

PHP序列化

有时需要把一个对象在网络上传输,为了方便传输,可以把整个对象转化为二进制串,等到达另一端时,再还原为原来的对象,这个过程称之为串行化(也叫序列化)。

有两种情况必须把对象序列化:
把一个对象在网络中传输
把对象写入文件或数据库

观察下面例子中对象在PHP序列化后的格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
class User
{
//类的数据
public $age = 0;
public $name = '';
//输出数据
public function printdata()
{
echo 'User '.$this->name.' is '.$this->age.' years old.<br />';
} // “.”表示字符串连接
}
//创建一个对象
$usr = new User();
//设置数据
$usr->age = 18;
$usr->name = 'Hardworking666';
//输出数据
$usr->printdata();
//输出序列化后的数据
echo serialize($usr)
?>

输出结果

1
2
User Hardworking666 is 18 years old.
O:4:"User":2:{s:3:"age";i:18;s:4:"name";s:14:"Hardworking666";}

“O”表示对象,“4”表示对象名长度为4,“User”为对象名,“2”表示有2个参数

“{}”里面是参数的key和value,

“s”表示string对象,“3”表示长度,“age”则为key;“i”是interger(整数)对象,“18”是value,后面同理。

下面是序列化格式的语法

a - array 数组型
b - boolean 布尔型
d - double 浮点型
i - integer 整数型
o - common object 共同对象
r - objec reference 对象引用
s - non-escaped binary string 非转义的二进制字符串
S - escaped binary string 转义的二进制字符串
C - custom object 自定义对象
O - class 对象
N - null 空
R - pointer reference 指针引用
U - unicode string Unicode 编码的字符串

序列化只序列属性,不序列方法。我们能控制的只有类的属性,攻击就是寻找合适能被控制的属性,利用作用域本身存在的方法,基于属性发动攻击

_sleep 方法在一个对象被序列化时调用,_wakeup方法在一个对象被反序列化时调用

反序列化漏洞

当传给unserialize()参数可控时,可以通过传入一个精心构造的序列化字符串,从而控制对象内部的变量甚至是函数。

示例

一个类用于临时将日志储存进某个文件,当__destruct被调用时,日志文件将会被删除:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//logdata.php
<?php
class logfile
{
//log文件名
public $filename = 'error.log';
//一些用于储存日志的代码
public function logdata($text)
{
echo 'log data:'.$text.'<br />';
file_put_contents($this->filename,$text,FILE_APPEND);
}
//destrcuctor 删除日志文件
public function __destruct()
{
echo '__destruct deletes '.$this->filename.'file.<br />';
unlink(dirname(__FILE__).'/'.$this->filename);
}
}
?>

调用这个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
include 'logdata.php'
class User
{
//类数据
public $age = 0;
public $name = '';
//输出数据
public function printdata()
{
echo 'User '.$this->name.' is'.$this->age.' years old.<br />';
}
}
//重建数据
$usr = unserialize($_GET['usr_serialized']);
?>

代码$usr = unserialize($_GET['usr_serialized']);中的$_GET['usr_serialized']是可控的,那么可以构造输入,删除任意文件。

如构造输入删除目录下的index.php文件:

1
2
3
4
5
6
<?php
include 'logdata.php';
$object = new logfile();
$object->filename = 'index.php';
echo serialize($object).'<br />';
?>

上面展示了由于输入可控造成的__destruct函数删除任意文件,其实问题也可能存在于__wakeup__sleep__toString等其他magic函数。

比如,某用户类定义了一个__toString,为了让应用程序能够将类作为一个字符串输出(echo $object),而且其他类也可能定义了一个类允许__toString读取某个文件。

绕过

__wakeup()函数

影响版本:
PHP5 < 5.6.25
PHP7 < 7.0.10

1
2
3
4
5
class xctf{
public $flag = '111';
public function __wakeup(){
exit('bad requests');
}

已知在使用 unserialize() 反序列化时会先调用 __wakeup()函数,

绕过 __wakeup()函数,即 在反序列化的时候不调用它

序列化的字符串中的 属性值 个数 大于 属性个数 就会导致反序列化异常,从而绕过 __wakeup()

漏洞防御

好的预防措施就是不要把用户的输入或者是用户可控的参数直接放进反序列化的操作中去