thinkphp中的RCE漏洞比较出名,在这里从源码层面分析学习。
RCE之调用任意类的任意方法
漏洞概述
ThinkPHP 底层没有对控制器名进行很好的合法性校验,导致在未开启强制路由的情况下,用户可以调用任意类的任意方法,最终导致rce的产生。
漏洞影响版本: 5.0.7<=5.0.x<=5.0.22 、5.1.0<=5.1.x<=5.1.30
payload:
1 | 5.0.x : |
漏洞分析
使用这个payload来分析:?s=index/\think\app/invokefunction&function=call_user_func_array&vars[]=system&vars[][]=id
使用thinkphp 5.0.10,默认有index的模块,发送以上payload,下断点调试。
从这里开始
跟进App::run()
,这里自动加载App类,跳出即可
run函数里先进行了初始化(self::initCommon()
),之后加载语言包,我们重点关注路由检测
跟进self::routeCheck
,这里先使用$request->path()
获取到了我们通过s
传入的值,之后进行路由检测。
在第563行,会读取配置中的url_route_must
值,表示是否强制使用路由,默认是false的,所以才能进入下一步。
官方最开始的修复方案之一就是将
url_route_must
设置为true
之后继续进入571行的Route::parseUrl
,可以看到这里将url解析为了模块、控制器和操作,而这三个都是从$path
数组中取得的,$path
数组由1212行的self::parseUrlPath
返回
跟进1212行的self::parseUrlPath
,可以看到就是将url以/
打散为数组
解析url完成后,得到了模块、控制器和操作。之后会使用self::exec
来执行请求。
跟进123行的exec
,执行的是一个模块
继续跟进,默认是支持多模块的,之后会判断模块是否存在(模块在application目录下,一个模块是一个文件夹),已经模块是否已经被禁用
之后获取控制器名和操作名,实例化控制器,最后调用self::invokeMethod
来反射调用函数。
可见这里没有一点点过滤的操作,只是判断了控制器是否存在,操作是否存在等,其本质就是判断类和类中的函数存不存在。
跟进invokeMethod
,先使用反射实例化了类,之后使用self::bindParams
获取参数,最后使用$reflect->invokeArgs
实现调用。
跟进self::bindParams
,看一下参数是怎么传进来的
可以看到使用了Request::instance()->param()
获取请求的参数,之后按照所调用的函数所需要的参数变量名来设置参数。
所以我们调用什么函数就传入该函数所需要的参数值就可以了
漏洞最终触发点在197行的$reflect->invokeArgs
在之后就是调用了我们传入的think\app
的invokefunction
函数,其执行我们传入的函数和参数
整个调用链为:
其实这整个过程就是是thinkphp的生命周期的一部分,问题出在没有对用户输入的控制器名字进行检测和过滤,导致可以实例化任意类,执行类中的任意方法,而且还可以自行传入参数。
漏洞修复
官方的修复方案是对控制器进行正则过滤
RCE之调用Request类任意方法
5.0.10开始默认debug为false
漏洞描述
thinkphp在获取method的方法中没有正确处理方法名,导致攻击者可以调用Request类任意方法并构造利用链,从而导致远程代码执行漏洞。
payload:
1 | 5.0.8 ~ 5.0.20(5.0.14及以后需要debug=true) |
漏洞分析
thinkphp5.0.8、 debug=false
使用的payload如下:
1 | POST /?s=index |
漏洞rce的执行点位于Request.php的filterValue
函数,的call_user_func
函数,这里通过控制传入filterValue
函数的参数$value
和$filters
来进行rce
从开始调试。
正常情况下,thinkphp经过了解析控制器名,操作名等之后调用invokeMethod
执行控制器类的方法,在invokeMethod
里,调用了self::bindParams
跟进,其调用了Request的param函数,用来获取请求变量
跟进param函数,之后根据请求方式为POST,调用了post方法获取POST的参数,之后再将post的参数与GET的参数进行合并,赋值给$this->param
。最后调用$this->input
,对其进行过滤。
跟进这里的input方法,这里参数name
值为空字符串。
之后使用$this->getFilter
获取到过滤器,再调用array_walk_recursive($data, [$this, 'filterValue'], $filter);
进行过滤操作。
先看看过滤器获取getFilter
函数,其实就是获取了类成员变量$this->filter
array_walk_recursive($data, [$this, 'filterValue'], $filter);
会将$data
数组中的每个元素传入filterValue
函数,将$filter
作为额外的参数
这里会调用call_user_func
,来将$value
作为参数调用$filter
,可以看到这里的$filter
为system,而$value
是post的数据一个一个以此传入。
那么问题来了,这个$filter
是在什么时候被赋值成为了system?
我们知道,这个filter其实来源于Request->filter
的成员变量,这里直接在Request
的构造函数中下断点,发现调用了两次__construct
第二次调用是在routeCheck
时调用的
在Route的check函数中,调用了$request->method()
方法,在这里下断点,进入method函数
这里thinkphp为了实现请求类型伪装,设置了var_method
的配置项,其默认值为_method
可见,这里可以通过控制_method
字段来进行类的任意方法调用,传入的参数为$_POST
因此,可以通过将_method
值设置为__construct
,从而再次调用构造函数,利用构造函数来覆盖filter
变量的值
总结一下:整个利用过程只需要一个请求,可以分成两步理解
第一步,利用请求类型伪装时的_method
参数没有进行过滤,调用Request的__construct
函数,重新覆盖Request对象的成员变量filter
。
用到的payload的
_method=__construct&filter=system
部分
调用栈:App::routeCheck
->Route::check
->Request::method
->Request::__construct
第二步,在使用Request的input函数来进行过滤输入参数的时候,触发rce
用到payload的
a=calc
部分
调用栈:App::exec -> App::module -> App::invokeMethod -> App::bindParams -> Request::param -> Request::input -> array_walk_recursive -> Request::filterValue -> call_user_func
thinkphp5.0.14、debug=true
使用的payload如下:
1 | POST /?s=index |
在thinkphp5.0.13中,App的module函数中添加设置了默认过滤器,导致之前我们通过__construct
覆盖的filter
成员变量会再次被修改
所以之前的利用链行不通,但是在开启了debug的情况下,在App的run
方法中,有记录路由和请求的代码,其中调用了$request->param()
,同样可以达到最后的rce
调用链:
thinkphp5.0.7、debug=false
1 | POST /?s=index HTTP/1.1 |
在thinkphp5.0.7中,Route中check函数在使用$request->method()
获取method之后,用这个method到self::$rules
中获取数据,在这里会产生错误,从而终止程序
在5.0.8中,这里的rules做了一个容错,所以不会报错
这里使用的payload增加了一个method=get
,因为method
也是Request
的一个成员变量,同样可以在执行Request::__construct
的时候将method进行重新赋值。所以这里使用method=get
将method
成员变量设置为get
,同时也达到了覆盖filter的目的。
调用链:
thinkphp5.0.21、debug=true
1 | POST / |
在5.0.21中,将Request的method
方法中获取原始请求类型的方式进行了修改
1 | // 在5.0.21之前 |
在server()
函数获取请求方法的时候,其内部调用了input函数来获取变量并进行过滤。
之前的payload_method=__construct&filter=system&a=whoami
在这一步的时候,会使用我们传入的过滤器system,执行system($_SERVER[REQUEST_METHOD])
,而返回空,从而使得method
函数返回值为GET
,从而之前的利用方法失败。
因此,这个版本的payload是_method=__construct&filter=system&server[REQUEST_METHOD]=whoami
通过覆盖$_SERVER[REQUEST_METHOD]
变量,使得其在input
的时候就进行rce
调用链:
漏洞修复
漏洞修复是在Request的method函数获取_method变量后对其进行过滤:
修复前:
修复后:
a