PHAR文件结构及其在反序列化漏洞中的利用
一、什么是 PHAR 文件
PHAR(PHp ARchive)是 PHP 的原生归档格式,类似于 Java 的 JAR 文件。它将多个 PHP 文件打包成一个文件,方便分发和应用。PHP 5.3 之后内置了 PHAR 支持。
一个典型的 PHAR 文件包含四个部分:
1 | ┌─────────────────────────┐ |
二、PHAR 文件结构详解
1. Stub(存根)
Stub 是 PHAR 文件的最开头部分,是一个 PHP 代码段,当 Phar 文件被 PHP 直接 include 或作为入口文件执行时运行。Stub 的固定格式如下:
1 | __HALT_COMPILER(); |
关键的是 __HALT_COMPILER(); —— 它是一个 PHP 编译器停止标记,告诉 PHP 解析器停止解析后面的内容。Stub 中还可以包含任意内容,因此可以利用这个特征将 PHAR 文件伪装成其他格式,比如:
1 | // 伪装成 GIF |
PHP 解析 __HALT_COMPILER() 时会跳过该行之后的所有内容,因此 Stub 后面的二进制数据不会被当作 PHP 代码执行。
Stub 是构造 PHAR 文件的必要部分——没有有效的 Stub,PHP 无法识别该文件为 PHAR 归档。
2. Manifest(清单)
Manifest 是 PHAR 文件的核心元数据区,也是整个漏洞利用的关键所在。它的结构如下:
1 | ┌─────────────────────────────────────┐ |
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 |
|
各部分在漏洞利用中的作用:
| 代码 | 对应结构 | 为什么必要 |
|---|---|---|
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.jpg、exploit.gif 等,绕过只检查后缀的上传限制。
四、PHAR 反序列化漏洞原理
核心问题
当 PHP 通过 phar:// 协议读取文件时,phar_stream_open() 内部会调用 phar_parse_metadata() 函数,该函数对 Manifest 中的 Metadata 使用 unserialize() 进行反序列化。
1 | // PHP 内核中的简化逻辑(位于 ext/phar/phar.c) |
这意味着只要有文件操作函数接收 phar:// 协议路径,就会触发 Metadata 的 unserialize(),调用流程图:
1 | phar://upload/evil.jpg |
触发函数列表
以下 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 反序列化需要满足:
- 存在文件操作函数调用:目标代码中有以上任意一个函数,且其参数部分可控
phar://协议未被禁用:stream_wrapper中phar可用(默认启用)- 能够上传 PHAR 文件:需要将构造好的 PHAR 文件存入目标服务器(任意目录),或者通过其他方式控制目标文件系统路径
- PHP 版本较旧:PHP 8.0 之后对
phar://协议的使用有限制(下面详述)
五、利用场景分析
场景一:文件包含 + 文件上传
1 | // 存在文件包含 |
上传名为 payload.jpg 的 PHAR 文件后,访问:
1 | http://target/index.php?file=phar://upload/payload.jpg |
触发流程:include('phar://upload/payload.jpg') → PHAR 解析 → Metadata 反序列化 → __destruct() 执行。
场景二:file_exists() 检查
1 | // 代码中不存在 unserialize,也没有文件包含 |
这种情况下代码看起来人畜无害,但攻击者只需找到任意文件上传点上传 PHAR 文件,通过 file_exists() 等简单函数就能触发反序列化。
场景三:GD 库图像处理
1 | // 图像上传后检查合法性 |
上传构造好的 PHAR/JPEG 混合文件即可触发。注意:这里 getimagesize() 既是触发函数,又是绕过目标(文件须符合图片格式),属于典型的双重利用。
六、绕过技巧总结
1. 协议可用性绕过
部分 PHP 配置会禁用某些协议,但 phar:// 默认一直可用。在 open_basedir 或 disable_functions 限制下尝试:
1 | // 其他支持 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() 等看似无害的文件操作函数中。
核心要点:
- PHAR 文件的 Metadata 字段会在
phar://协议解析时被自动反序列化 - 触发函数极其广泛(文件操作、图像处理、包含加载等多达 60+ 个函数)
- 上传接口 + phar:// 协议 = 反序列化漏洞,即使代码中没有显式的
unserialize() - Stub 的灵活构造使 PHAR 文件可以伪装成任意格式
- 构造 PHAR 文件的四个必要部分:Stub、Metadata(恶意对象)、至少一个文件、有效签名
