2019年1月11日,ThinkPHP官方发布安全更新,修复了一个GETSHELL漏洞。现分析如下。
漏洞复现
以 thinkphp 5.0.22 完整版为例,下载地址:http://www.thinkphp.cn/down/1260.html
未开启调试模式。
|
|
漏洞分析之POC 1
先整体的看一下这个流程,tp程序从 App.php
文件开始,其中截取部分如下:
在App.php
中,会根据请求的URL调用routeCheck
进行调度解析获得到$dispatch
,之后将进入exec($dispatch, $config)
根据$dispatch
类型的不同来进行处理。
在payload中,访问的url为index.php?s=captcha
。在vendor/topthink/think-captcha/src/helper.php
中captcha注册了路由,
因此其对应的dispatch
为method
:
一步步跟入,其调用栈如下:
通过调用Request
类中的method
方法来获取当前的http请求类型,这里顺便贴一下该方法被调用之处:
该函数的实现在 thinkphp/library/think/Request.php:512
在tp的默认中配置中设置了表单请求类型伪装变量如下
因此通过POST一个_method
参数,即可进入判断,并执行$this->{$this->method}($_POST)
语句。因此通过指定_method
即可完成对该类的任意方法的调用,其传入对应的参数即对应的$_POST
数组
Request
类的构造函数__construct
代码如下
利用foreach循环,和POST传入数组即可对Request
对象的成员属性进行覆盖。其中$this->filter
保存着全局过滤规则。经过覆盖,相关变量变为:
注意我们请求的路由是?s=captcha
,它对应的注册规则为\think\Route::get
。在method
方法结束后,返回的$this->method
值应为get
这样才能不出错,所以payload中有个method=get
。在进行完路由检测后,执行self::exec($dispatch, $config)
,在thinkphp/library/think/App.php:445,由于$dispatch
值为method
,将会进入如下分支:
跟入Request::instance()->param()
,该方法用于处理请求中的各种参数。
如上方法中$this->param
通过array_merge
将当前请求参数和URL地址中的参数合并。回忆一下前面已经通过__construct
设置了$this->get
为dir
。此后$this->param
其值被设置为:
继续跟入$this->input
:
该方法用于对请求中的数据即接收到的参数进行过滤,而过滤器通过$this->getFilter
获得:
前面$this->filter
已经被设置为system
,所以getFilter
返回后$filter
值为:
回到input
函数,由于$data
是前面传入的$this->param
即数组,所以接着会调用array_walk_recursive($data, [$this, 'filterValue'], $filter)
,对$data
中的每一个值调用filterValue
函数,最终调用了call_user_func
执行代码:
扩展之POC 2
回想前面的调用链,param -> method -> input -> getFilter -> rce。因为filter
可控,而tp的逻辑会对输入即input进行filter
过滤,所以重点是找到一个合理的input
入口。
回到param
方法:
跟入$this->method(true)
注意此时的参数为true
,所以此处会进入第一个分支:
继续跟入$this->server
,可以发现这里也有一个input
!
所以对input
方法而言,其$data
即$this->server
数组,其参数name
值为REQUEST_METHOD
,在input
方法源码如下:
因此利用前面的__construct
,可以通过传入server[REQUEST_METHOD]=dir
,使得在经过foreach
循环时置$data
值为dir
,此后调用getFilter
,同样实现RCE:
给出payload:
补丁分析
补丁地址:https://github.com/top-think/framework/commit/4a4b5e64fa4c46f851b4004005bff5f3196de003
问题的根源在于请求方法的获取接收了不可信数据,因此补丁中设置了白名单,如下
其他
这里仅仅测试了5.0.22 完整版本。各个版本之间代码有些许差异,payload不一定通用,建议自己调试调试。