ThinkPHP 5.0.x RCE漏洞

Request类中的变量覆盖导致rce(5.0.8-5.0.23)

Posted by SEVENTEEN on May 20, 2021

前言

   最近反序列化漏洞分析了几个, 准备来分析一下我觉得难度不小的ThinkPHP5.x RCE漏洞。

配置环境

   在TP官网下载5.022核心版。

利用条件

   5.0.8 <= 5.x <= 5.0.23

利用链如下

   该利用链不短,debug涉及的点也比较多,从入口点开始分析。 搜索一下call_user_func函数触发点。

   入口点位于:/thinkphp/library/think/Request.php, 传入的第三个参数为回调函数名,第一个参数为传入函数的参数, 所以需要控制$filter与$value的值,才能命令执行。

   查看一下filterValue方法的调用情况, 可以看到无论$data是不是数组最终都会调用filterValue方法(但其实在后面的param()方法会把$data变成数组), 而$filter通过1023行的过滤器$this->getFilter($filter, $default)方法来获取。

   查看一下getFilter方法,这里默认$filter为空, 所以将$this->filter赋值给$filter。最后filter[] = $default, 即给filter[]添加一组空数据。所以,返回的其实是 [$this->filter,null]。

   回到input()方法,这里的array_walk_recursive($data, [$this, 'filterValue'], $filter), 意思是将$filter传入$this->filterValue(),再将$data中每一个元素使用$this->filterValue()。 (上面分析过call_user_func的参数,$filter为回调函数名,$data为传入回调函数的参数)。

   查看一下input()方法的调用情况,康康传入$data是什么。 来到同文件下的param方法,可以看出无论在659行还是661行的return中调用input()方法, $data总是652行的$this->param,即请求参数和URL地址中的参数合并后的数组, 所以652行的$this->param中只要控制其中一个就可以了。 接着来看一下怎么控制$filter(回调函数名)跟$data(回调函数参数)就可以了。

   来到同文件下的method方法,$this->method默认为false, 并且在配置文件中,Config::get('var_method')在默认情况下是_method,所以总会进入524行的if条件中。 而在526行的$this->{$this->method}($_POST), 我们可以控制$this->method的值,即可以任意调用Request类中存在的任何方法以及控制传入的参数。

   既然可以任意调用Request类中存在的任何方法以及通过POST参数控制传入的参数, 那么就可以调用Request类的__construct方法来控制Request类中的变量。

_method=__construct&filter[]=system&get[]=whoami或者 _method=__construct&filter[]=system&route[]=whoami

   那么如何控制Request类中的参数,从而控制call_user_func($filter,$value)函数中的参数呢? 首先回来看一下$filter参数如何赋值的,上面分析过$filter参数默认为空,会取$this->filter的值。而$this->filter在Request类中, 我们令filter[]=system即可控制$this->filter的值。

令filter[]=system

   控制了call_user_func($filter,$value)函数中的$filter后,我们还需要控制一下$value的值。 上面分析过,$value其实是$data参数,而$data总是652行的$this->param。

   所以,只需要控制一下$this->param, 而$this->param通过array_merge()函数去取值,查看一下array_merge()函数中的$this->get()方法跟$this->route()方法。 会发现这两个方法的返回值中总带有$this->get或$this->route,所以要覆盖get或者route参数。 不过这里为什么不覆盖第一个参数$this->param呢?其实你看一下route()方法, 就会发现该方法内对这个变量进行了一次初始化,导致无法控制该参数。

令get[]=whoami或者route[]=whoami

   接下来,就需要看一下怎么调用Request类的method方法以及param方法。 在thinkphp/library/think/Route.php,调用了$request->method()。

   再来查看一下Route::check的调用情况。 找到thinkphp/library/think/APP.app中的routeCheck方法。

   发现在APP:run()中调用了routeCheck方法, 并且APP:run()为应用启动类,每次都会在执行应用时被调用。所以,需要让$dispatch为空。

   查看一下Hook::listen('app_dispatch', self::$dispatch), 调试了一下,self::$dispatch默认为空。所以,能调用$request->method()方法。

   接下来,来看一下Request::param的调用。 发现在thinkphp/library/think/App.php::run()有调用,但需要满足开启debug模式。

POC(调试模式开启)

POST: _method=__construct&filter[]=system&get[]=whoami
或者
POST: _method=__construct&filter[]=system&route[]=whoami

   但在实际渗透中,很多网站并不会开启debug模式,所以在上面利用链的基础上,还需要找别的利用点来调用Request::param。 全局搜索调用栈,还找到了在thinkphp/library/think/App.php::run()中会调用的thinkphp/library/think/App.php::exec()方法, 这里需要满足$dispatch['type']==method或者$dispatch['type']==controller时,才会调用param()方法。

   而$dispatch = self::routeCheck($request, $config), 所以回过头看一下thinkphp/library/think/App.php::routeCheck方法。 这里要让$result['type']==method或者$result['type']==controller才能满足$dispatch['type']==method或者$dispatch['type']==controller

   跟进一下self::routeCheck方法,可以看到这里当路由执行为路由到方法或者路由到控制器时, 可以满足$result['type']==method或者$result['type']==controller。

   而TP自带的验证码组件captcha注册了一个get路由,我们只需要通过前面的__destruct方法来对method覆盖为get, 然后调用验证码组件captcha即可。

令method=get

POC(调试模式未开启)

GET: s=captcha

POST: _method=__construct&method=get&filter[]=system&get[]=ls
或者
POST: _method=__construct&method=get&filter[]=system&route[]=ls

There Is Nothing Below

   

Turn at the next intersection.