一、什么是 PHAR 文件

PHAR(PHp ARchive)是 PHP 的原生归档格式,类似于 Java 的 JAR 文件。它将多个 PHP 文件打包成一个文件,方便分发和应用。PHP 5.3 之后内置了 PHAR 支持。

一个典型的 PHAR 文件包含四个部分:

1
2
3
4
5
6
7
8
9
┌─────────────────────────┐
│ Stub │ ← PHAR 文件头
├─────────────────────────┤
│ Manifest │ ← 元数据(含反序列化攻击面)
├─────────────────────────┤
│ File Contents │ ← 被归档的文件内容
├─────────────────────────┤
│ Signature │ ← 文件签名(可选)
└─────────────────────────┘

二、PHAR 文件结构详解

1. Stub(存根)

Stub 是 PHAR 文件的最开头部分,是一个 PHP 代码段,当 Phar 文件被 PHP 直接 include 或作为入口文件执行时运行。Stub 的固定格式如下:

1
<?php __HALT_COMPILER(); ?>

关键的是 __HALT_COMPILER(); —— 它是一个 PHP 编译器停止标记,告诉 PHP 解析器停止解析后面的内容。Stub 中还可以包含任意内容,因此可以利用这个特征将 PHAR 文件伪装成其他格式,比如:

1
2
3
4
5
6
7
8
9
10
11
// 伪装成 GIF
GIF89a<?php __HALT_COMPILER(); ?>

// 伪装成 JPEG
\xFF\xD8\xFF\xE0<?php __HALT_COMPILER(); ?>

// 伪装成 PDF
%PDF-1.4<?php __HALT_COMPILER(); ?>

// 前面加空格绕过头部校验
<?php __HALT_COMPILER(); ?>

PHP 解析 __HALT_COMPILER() 时会跳过该行之后的所有内容,因此 Stub 后面的二进制数据不会被当作 PHP 代码执行。

Stub 是构造 PHAR 文件的必要部分——没有有效的 Stub,PHP 无法识别该文件为 PHAR 归档。

2. Manifest(清单)

Manifest 是 PHAR 文件的核心元数据区,也是整个漏洞利用的关键所在。它的结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
┌─────────────────────────────────────┐
│ Manifest Length (4 bytes) │ ← 整个 Manifest 的长度
├─────────────────────────────────────┤
│ Number of Files (4 bytes) │ ← 归档内文件数量
├─────────────────────────────────────┤
│ Version (2 bytes) │ ← PHAR 版本号(API version)
├─────────────────────────────────────┤
│ Global Phar Bitmap (4 bytes) │ ← 全局标志位
├─────────────────────────────────────┤
│ Metadata Length (4 bytes) │ ← Metadata 序列化后的长度
├─────────────────────────────────────┤
│ Metadata (serialized data) │ ← ⚠️ 反序列化攻击面!
├─────────────────────────────────────┤
│ File Entry 1: Filename Length │
│ File Entry 1: Filename │
│ File Entry 1: File Header... │
├─────────────────────────────────────┤
│ File Entry 2: ... │
└─────────────────────────────────────┘

Metadata 字段是关键:当 PHP 通过 phar:// 协议访问 PHAR 文件时,无论使用的是 file_get_contents()file_exists() 还是 include,PHP 都会自动将 Manifest 中的 Metadata 字段反序列化。

也就是说,攻击者可以在构建 PHAR 文件时,将一个精心构造的 PHP 对象序列化后写入 Metadata 字段,当目标代码通过 phar:// 协议访问该文件时,这个对象就会被自动反序列化,从而触发 POP 链。

Metadata 是构造反序列化 PHAR 文件的必要部分——没有 Metadata,就不会有反序列化触发点。

3. File Contents(文件内容区)

存储归档中的实际文件内容,每个文件在 Manifest 中都有对应的 File Entry 描述其文件名、大小、压缩方式、权限等信息。

至少一个文件是构造 PHAR 文件的必要部分——PHAR 归档必须包含至少一个文件条目,否则 PHP 会将其视为无效归档。

4. Signature(签名)

PHAR 文件的末尾有 8 字节的签名标记:

1
signature (可变长度) + format (4 bytes) + magic (4 bytes)

签名类型(format):

算法
1 MD5
2 SHA1
3 SHA256
4 SHA512

Magic 固定为 GBMB,即 \x47\x42\x4d\x42

需要注意的是,签名并非必须的。Phar::stopBuffering() 时如果不主动设置签名,或者手动构造 PHAR 文件时省略签名部分,PHP 依然可以正常解析。

三、构造PHAR反序列化文件的必要部分

使用 PHP 内置的 Phar 类构造一个用于反序列化漏洞利用的 PHAR 文件,需要四个必要步骤,对应 PHAR 文件的四个核心结构:

1
2
3
4
5
6
7
8
9
<?php
// 确保 phar.readonly = Off(运行: php -d phar.readonly=0 gen.php)
$phar = new Phar('exploit.phar');
$phar->startBuffering();
$phar->addFromString('dummy.txt', ''); // 必要:至少一个文件
$phar->setMetadata($maliciousPayload); // 必要:恶意序列化数据
$phar->setStub("<?php __HALT_COMPILER(); ?>"); // 必要:有效存根
$phar->stopBuffering();
?>

各部分在漏洞利用中的作用:

代码 对应结构 为什么必要
new Phar() 创建 PHAR 对象,开始构建
startBuffering() 启动缓冲,允许在写入前修改
addFromString() File Contents PHAR 归档必须包含至少一个文件,否则解析失败
setMetadata() Manifest → Metadata 核心:将恶意对象序列化后存入 Metadata,触发反序列化的源头
setStub() Stub 必须包含 __HALT_COMPILER();,否则 PHP 不识别为 PHAR
stopBuffering() Signature 完成写入并生成签名,使文件成为完整有效的 PHAR

运行生成命令:

1
php -d phar.readonly=0 gen_phar.php

生成后的 exploit.phar 可以根据需要重命名为 exploit.jpgexploit.gif 等,绕过只检查后缀的上传限制。

四、PHAR 反序列化漏洞原理

核心问题

当 PHP 通过 phar:// 协议读取文件时,phar_stream_open() 内部会调用 phar_parse_metadata() 函数,该函数对 Manifest 中的 Metadata 使用 unserialize() 进行反序列化。

1
2
3
4
5
6
// PHP 内核中的简化逻辑(位于 ext/phar/phar.c)
phar_entry_info *phar_get_entry_info(phar_archive_data *phar, ...) {
// ...
phar->metadata = php_var_unserialize(phar->metadata); // ← 自动反序列化
// ...
}

这意味着只要有文件操作函数接收 phar:// 协议路径,就会触发 Metadata 的 unserialize(),调用流程图:

1
2
3
4
5
6
7
phar://upload/evil.jpg
→ phar_stream_open() // 打开 phar 流
→ phar_parse_pharfile() // 解析 PHAR 文件结构
→ phar_parse_metadata() // 解析 Manifest 中的 Metadata
→ php_var_unserialize() // ← 反序列化 Metadata!
→ __wakeup() / __destruct() / ... // 触发魔术方法
→ POP 链执行

触发函数列表

以下 PHP 文件操作函数在使用 phar:// 协议时,都会触发反序列化:

文件存在性检查(常见触发点):

  • file_exists() — 最常见的检查函数
  • is_file() / is_dir() / is_link() / is_writable() / is_readable() / is_executable()
  • filetype()

文件内容读取:

  • file_get_contents() — 读取文件内容
  • fopen() / fread() / fgets() 等文件句柄操作
  • readfile() — 直接输出文件内容
  • file() — 按行读取文件到数组
  • parse_ini_file() — 解析 ini 配置文件

文件信息获取:

  • filesize() / filectime() / filemtime() / fileatime()
  • stat() / lstat() / fstat()
  • fileowner() / filegroup() / fileperms() / fileinode()

目录操作:

  • scandir() / opendir() / readdir()
  • glob()(在某些情况下使用 phar://

包含与执行:

  • include / include_once / require / require_once — PHP 语句
  • highlight_file() / show_source()

写入与删除:

  • unlink() — 删除文件
  • copy() — 复制文件(源路径为目标路径会破坏目标文件)
  • rename() — 重命名
  • file_put_contents() — 写入内容
  • touch() — 修改文件时间
  • chmod() / chown() / chgrp() — 修改权限

图像处理函数(GD 库):

  • getimagesize() / getimagesizefromstring()
  • exif_thumbnail() / exif_read_data()
  • imageloadfont() / imagecreatefromgif() / imagecreatefrompng() / imagecreatefromjpeg()

其他:

  • md5_file() / sha1_file() / hash_file() — 计算文件哈希
  • finfo->file() — 检测文件 MIME 类型
  • mime_content_type()

利用条件

成功利用 PHAR 反序列化需要满足:

  1. 存在文件操作函数调用:目标代码中有以上任意一个函数,且其参数部分可控
  2. phar:// 协议未被禁用stream_wrapperphar 可用(默认启用)
  3. 能够上传 PHAR 文件:需要将构造好的 PHAR 文件存入目标服务器(任意目录),或者通过其他方式控制目标文件系统路径
  4. PHP 版本较旧:PHP 8.0 之后对 phar:// 协议的使用有限制(下面详述)

五、利用场景分析

场景一:文件包含 + 文件上传

1
2
3
4
5
6
// 存在文件包含
$file = $_GET['file'];
include($file); // 可以用 phar:// 触发

// 存在文件上传,只检查后缀
move_uploaded_file($_FILES['file']['tmp_name'], 'upload/' . $_FILES['file']['name']);

上传名为 payload.jpg 的 PHAR 文件后,访问:

1
http://target/index.php?file=phar://upload/payload.jpg

触发流程:include('phar://upload/payload.jpg') → PHAR 解析 → Metadata 反序列化 → __destruct() 执行。

场景二:file_exists() 检查

1
2
3
4
// 代码中不存在 unserialize,也没有文件包含
if (file_exists($filename)) { // phar:// 触发反序列化
echo "File found";
}

这种情况下代码看起来人畜无害,但攻击者只需找到任意文件上传点上传 PHAR 文件,通过 file_exists() 等简单函数就能触发反序列化。

场景三:GD 库图像处理

1
2
3
4
// 图像上传后检查合法性
$size = getimagesize($uploaded_file); // 同时触发 phar 反序列化
// 或
$im = imagecreatefromjpeg($uploaded_file);

上传构造好的 PHAR/JPEG 混合文件即可触发。注意:这里 getimagesize() 既是触发函数,又是绕过目标(文件须符合图片格式),属于典型的双重利用。

六、绕过技巧总结

1. 协议可用性绕过

部分 PHP 配置会禁用某些协议,但 phar:// 默认一直可用。在 open_basedirdisable_functions 限制下尝试:

1
2
3
4
5
// 其他支持 PHAR 的协议变体
phar://
compress.zlib://phar://
compress.bzip2://phar://
php://filter/resource=phar://

2. 签名绕过

部分 PHP 版本在解析 PHAR 时,如果文件末尾不符合签名格式,会直接报错。但是对于 phar 内容完全可控 的场景,重新计算签名即可。

另一种做法是利用 Phar::stopBuffering() 前主动设置签名类型:

1
$phar->setSignatureAlgorithm(Phar::SHA1);

3. 文件上传限制绕过

服务端检查 绕过方法
只检查后缀 Stub 中添加图片头即可
检查 MIME 修改 Content-Type: image/jpeg
getimagesize() 检查 使用真实图片头 + PHAR 拼接(需精确拼接)
exif_imagetype() 检查 同上,需有效图像头
文件内容长度限制 缩小 PHAR 文件体积(只保留一个空文件)
文件名 hash 后不可预测 仍需找到信息泄露或其他途径获取路径

4. PHP 版本限制

PHP 版本 PHAR 反序列化状态
5.3 - 7.x 完全可用,默认触发
8.0 - 8.3 仍然可用,部分函数有参数校验增强
8.0+ (特定修复) unlink() 等写入型函数限制了 phar:// 路径的传入

七、总结

PHAR 反序列化漏洞的发现改变了 PHP 安全领域对反序列化的认知。在传统的反序列化漏洞中,开发者知道要警惕 unserialize() 函数;而 PHAR 反序列化的威胁在于,它通过 phar:// 协议将反序列化”隐藏”在了 file_exists()getimagesize() 等看似无害的文件操作函数中。

核心要点:

  1. PHAR 文件的 Metadata 字段会在 phar:// 协议解析时被自动反序列化
  2. 触发函数极其广泛(文件操作、图像处理、包含加载等多达 60+ 个函数)
  3. 上传接口 + phar:// 协议 = 反序列化漏洞,即使代码中没有显式的 unserialize()
  4. Stub 的灵活构造使 PHAR 文件可以伪装成任意格式
  5. 构造 PHAR 文件的四个必要部分:Stub、Metadata(恶意对象)、至少一个文件、有效签名