Phar(PHP Archive)是一种将多个 PHP 文件及资源打包成单个文件(类似 Java 的 JAR 或 ZIP)的 PHP 归档格式,方便 PHP 应用程序和库的分发,支持 Tar、ZIP 或自定义格式,并可通过 Gzip/Bzip2 压缩和数字签名,主要通过 php.net/manual/zh/book.phar.php 介绍的 Phar 类进行创建和操作,但需注意其反序列化机制曾存在安全漏洞。 Phar 在PHP 5.3 或更高版本中默认开启
前戏
注意要将php.ini中的phar.readonly选项设置为Off,否则无法生成phar文件。如果懒得关或找不到,可以临时使用php -d phar.readonly=0 gen_phar.php
先来看phar的结构
-
a stub 可以理解为一个标志,格式为xxx,前面内容不限,但必须以__HALT_COMPILER();来结尾,否则phar扩展将无法识别这个文件为phar文件
-
a manifest describing the contents phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是上述攻击手法最核心的地方
-
the file contents 被压缩文件的内容
-
[optional] a signature for verifying Phar integrity (phar file format only) 签名,放在文件末尾
Metadata反序列化
说到Phar在CTF里的应用,我们常常考虑到phar的Metadata反序列化,当Phar被phar://解析时,会自动触发metadata的反序列化。
来看exp
<?php class TestObject { }
$phar = new Phar("exploit.phar"); // 这里有一个Trick(后面会讲)。后缀不强制,只要整个文件名包含了.phar即可,意味着exploit.phar.png也是合法的 $phar->startBuffering(); $phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub,这是一个标志 $o = new TestObject(); // 这里构造pop链 $phar->setMetadata($o); // 将自定义的meta-data存入manifest,这个$o即为你构造的反序列化链 $phar->addFromString("test.txt", "test"); // 添加要打包的东西,即便不使用,也最好添加一个防止报错 // 签名自动计算 $phar->stopBuffering();?>Phar爱上了Include(LFI),他却想无套(压缩壳)内射(包含)?
当phar被压缩时,php对其有着相当奇妙的特性。
一定一定一定要看 这位师傅的文章
上面那位师傅讲得非常透彻!并且深入底层从源码角度展开了探索。这里总结如下:
- 当phar使用gzip、bz2压缩,并且被include时,会触发自动解压。
- 自动解压只需要文件名带有
.phar即可,甚至在路径里都可以!(后缀不重要)
但实战中我有遇到过打的phar包含没有执行,仔细分析一下,这里补充第三条。
- 未压缩的
.phar会把stub当作入口执行;压缩的phar会尝试执行内部的index.php。
下面具体展开讲讲第三条。
这里给出一个例子(带压缩的phar):
<?php@unlink("exploit.phar");@unlink("exploit.phar.gz");$phar = new Phar('exploit.phar');$phar->startBuffering();$code1 = <<<'EOD'<?php echo "Phar stub executed\n"; __HALT_COMPILER();?>EOD;
$code2 = <<<'EOD'<?php echo "Phar index.php executed\n";?>EOD;
$phar->setStub($code1);$phar->addFromString('index.php', $code2);
$phar->compress(Phar::GZ);
$phar->stopBuffering();
?>程序很简单,分别往stub和index.php里写了code1和code2,然后gz压缩生成exploit.phar.gz。那么问题来了,此时执行include("exploit.phar"); 究竟执行了code1还是code2呢?
再给出一个例子(纯phar):
<?php@unlink("exploit.phar");$phar = new Phar('exploit.phar');$phar->startBuffering();$code1 = <<<'EOD'<?php echo "Phar stub executed\n"; __HALT_COMPILER();?>EOD;
$code2 = <<<'EOD'<?php echo "Phar index.php executed\n";?>EOD;
$phar->setStub($code1);$phar->addFromString('index.php', $code2);
// 注意这里不再进行压缩// $phar->compress(Phar::GZ);
$phar->stopBuffering();
?>依然是分别往stub和index.php里写了code1和code2,但不进行压缩。这时候执行include("exploit.phar");,到底会执行code1还是code2呢?
我们实操一下

结果非常Amazing啊!可以看到,当phar戴套(压缩后),直接include压缩包会执行index.php中的代码。为什么呢?因为当phar不进行压缩时,stub会以明文形式出现在phar头部,而其他文件均以资源形式存贮,除非制定phar内的某段代码,否则不会执行。

而当phar压缩后,实际上php明文已经消失了(这也是绕过文件上传waf的关键)。这个时候,php会尝试解压phar并且自动包含内部的index.php。因此,当使用压缩的方式上传phar绕过waf,并且可控的包含路径使用了basename或者过滤了/时,可以把payload放置在index.php里,当包含压缩phar时会自动执行。
总结:未压缩的 .phar 会把 stub 当作 PHP 入口执行;一旦压缩成 .phar.gz / .phar.bz2,stub 不再是明文入口,PHP 会先解压并把 phar 当作“文件系统”处理,include 时自动落到内部的 index.php,从而执行其中的代码。
另外补充一点我老是搞不清楚的东西:
Phar::compressFiles()和Phar::compress()的区别:
| 特性 | Phar::compress() | Phar::compressFiles() |
|---|---|---|
| 压缩对象 | 整个 phar 文件 | 仅 phar 内文件内容 |
| 输出文件 | 新文件(.phar.gz/.phar.bz2) | 原 phar 文件 |
| stub 行为 | include时不会执行stub | stub是原文,LFI是会执行stub |
| metadata | 包含在压缩流里 | metadata 不变 |
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时









