1、写在前面
OK 兄弟们,这几天一直在面试,发现很多 HR 喜欢问反序列化相关的内容,今天咱们就从最简单的 PHP 原生反序列化入手,带大家入门反序列化
2、PHP 序列化
在 PHP 中,有反序列化,就有序列化,我们先来解释一下序列化。
所谓序列化,就是将 PHP 的一个对象,序列化为一串字符串的过程,其所用的函数就是serialize()
对象:一个对多种信息描述的整体
类:一个储存信息的共享的模板,以class开头
我们先试着创建一个新的类
<?php
class seren //定义一个类
{
//变量public $a = 'hello';private $b = 'world';
//方法public function print_var()
{echo $this->a;}
}
?>
上面就是一个很简单的类,我们来看一下,首先使用class声明了一个类seren,在这个seren类里,我们有两个变量a和b,还有一个方法print_var
现在我们要去使用这个类,就需要把这个类实例化为一个对象
//创建一个对象
$object = new seren();
//调用一个方法
$object->print_var();
我们可以使用new,将seren类实例化为一个object对象,如果我们想要使用这个类的方法,可以直接使用 $object->print_var(); 的方法来调用这个方法。
接下来,我们试着使用serialize函数,将这个对象给序列化
<?php
class seren //定义一个类
{
//变量public $a = 'hello';private $b = 'world';
//方法public function print_var()
{echo $this->a;}
}
$object = new seren();
$c = serialize($object);
echo $c;
?>
此处我们使用 serialize 将object这个对象,序列化为c,并打印出c
此处可以看到打印出的字符串
O:5:"seren":2:{s:1:"a";s:5:"hello";s:8:" seren b";s:5:"world";}
接下来我们就把这串字符串逐个解释一下。
首先是前面的 O,代表这是一个Object,也就是一个对象,然后是 O 后面的5,代表这个对象的名称为 5 个长度,再后面的seren就是我们对象的名称,后面的 2 就是我们对象里变量的个数。
然后就是大括号里面的内容,里面都是对我们对象里的变量的描述。
第一个s:1:"a";
,表示为string类型,这个变量的名称长度为 1,变量名称为 a
后面的s:5:"hello";
,表示为string类型,这个值的长度为 5,值为 hello
其实从这里我们就可以发现出规律,这个字符串对对象的描述都是类型+长度+名称的格式
比较特殊的就是后面的s:8:" seren b";
,此处我们可以看到似乎有点不一样,这里的长度变成了 8,并且名称变成了serenb
我们回到上面,看我们创建这个类的时候,b 的类型为private,所以就可以知道,根据规定,private类型的变量在序列化后,名称会变为类名+变量名的格式,但是serenb也只有 6 个长度,但是他提示我们有 8 个长度
其实是因为在seren的前后有两个我们不可见的%00,他真正的内容应该是%00seren%00b,%00 算是一个长度,所以一共是 8 个长度。
3、PHP 反序列化
讲完了序列化,接下来就是反序列化,反序列化的函数是unserialize(),他可以将我们的序列化后的字符串,反序列化为对象
<?php
class seren //定义一个类
{
//变量public $a = 'hello';private $b = 'world';
//方法public function print_var()
{echo $this->a;}
}
$object = new seren();
$c = serialize($object);
$object2 = unserialize($c);
var_dump($object2);
?>
此处我们使用unserialize函数将字符串 c 反序列化为了对象object2
4、魔术方法
魔术方法是 php 反序列化漏洞利用中很重要的一环,一般以两个下划线开头,如果这个类里面存在魔术方法,他就会优先调用魔术方法
_construct() //当一个对象创建时被调用
_destruct() //对象被销毁时触发
_wakeup() //反序列化前触发
_sleep() //序列化前触发
_toString() //把类当作字符串使用时触发
_get() //用于从不可访问的属性读取数据
_set() //用于将数据写入不可访问的属性
_isset() //在不可访问的属性上调用isset()或empty()触发
_unset() //在不可访问的属性上使用unset时触发
_invoke() //当脚本尝试将对象调用为函数时触发
我们试着在类里创建一个魔术方法wakeup
<?php
class seren //定义一个类
{
//变量public $a = 'hello';private $b = 'world';
//方法public function print_var()
{echo $this->a;}public function __wakeup()
{// TODO: Implement __wakeup() method.echo 'Follow SerenSec!';}
}
$object = new seren();
$c = serialize($object);
$object2 = unserialize($c);
?>
运行后发现输出了Follow SerenSec!
5、PHP 反序列化漏洞
我们可以试着在反序列化的时候,寻找一些比较危险的魔术方法,从而达成 RCE 的目的,这就是反序列化漏洞
下面是一个很简单的例子
<?php
class seren //定义一个类
{
//变量public $a = 'hello';private $b = 'world';public $hack;
//方法public function print_var()
{echo $this->a;}public function __construct($hack) {$this->hack = $hack;}public function __wakeup()
{// TODO: Implement __wakeup() method.echo shell_exec($this->hack);}
}
$object2 = unserialize($_GET['hack']);
?>
此时我们发现有 __wakeup 魔术方法,其中存在 shell_exec 函数,可以执行命令,并且其中的 hack 变量是可控的
我们先构造出序列化后的字符串
O:5:"seren":3:{s:1:"a";s:5:"hello";s:8:"%00seren%00b";s:5:"world";s:4:"hack";s:6:"whoami"}
然后构造 payload
http://localhost/test.php?hack=O:5:%22seren%22:3:{s:1:%22a%22;s:5:%22hello%22;s:8:%22%00seren%00b%22;s:5:%22world%22;s:4:%22hack%22;s:6:%22whoami%22}
成功执行命令
6、结语
PHP 原生反序列化比较简单,后面会有一些拓展内容,像是POP 链
和Phar 协议
,这些是比较绕的地方,各位可以自行探索。