ThinkPHP 5.x全版本任意代码执行

  1. 0x01 漏洞环境
  2. 0x02 漏洞分析
  3. 0x03 修复方案

author: Dlive

0x01 漏洞环境

影响版本: 5.0 ~ 5.0.22, 5.1 ~ 5.1.30

调试环境:
thinkphp 5.0.20

利用条件:
没有开启强制路由(默认不开启)

参考:
https://github.com/vulhub/vulhub/tree/master/thinkphp/5-rce
http://www.thinkphp.cn/topic/60400.html

0x02 漏洞分析

这个漏洞的原理还是比较简单的,问题出在了实例化控制器的地方,之前我们说过在框架代码审计中,框架如何处理路由是需要重点关注的一个点。

路由处理逻辑需要关注的地方有:

  1. 框架如何根据URL找到用户访问的类/方法
  2. 框架如何根据URL找到用户访问脚本文件
  3. 框架如何处理静态文件的访问(django/tornado/spring/express都在这上面出过问题)

下面我们看一下漏洞的整体流程

exp如下

1
http://127.0.0.1:8080/index.php?s=index/think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=ls

tp 5.1还可以使用input filter执行代码,这也是比较常见的一种tp执行代码的方法,可被用于制作webshell

1
http://127.0.0.1:8080/index.php?s=index/think\request/input?data[]=ls&filter=system

thinkphp框架由App->run()为入口执行

$dispatch存储了thinkphp进行MVC调度/路由的信息,比如用户请求的模块、控制器、操作(action)
self::routeCheck对URL进行检查和解析
thinkphp/library/think/App.php think\App::run()

parseUrl用于解析URL
thinkphp/library/think/App.php think\App::routeCheck()

parseUrl调用parseUrlPath进行进一步URL解析
thinkphp/library/think/Route.php think\Route::parseUrl()

parseUrlPath将URL以/分割,得到模块名、控制器名、操作函数名
thinkphp/library/think/Route.php think\Route::parseUrlPath()

而之后,控制器类名直接被实例化,然后调用操作函数,中间没有任何合法性校验,那么我们就可以通过\使用namespace调用已加载的thinkphp内置类,比如think\app,之后该类会被实例化,然后invokefunction将被调用,
thinkphp/library/think/App.php think\App::run()

thinkphp/library/think/App.php think\App::exec()

thinkphp/library/think/App.php think\App::module()
Loader::controller中通过反射实例化名为$controller的类

实例化think\App类,调用其中的invokefucntion方法
thinkphp/library/think/App.php think\App::module()

0x03 修复方案

直接白名单限制了控制器名中允许的字符

1
2
3
4
5
6
// library/think/App.php
// 获取控制器名
$controller = strip_tags($result[1] ?: $config['default_controller']);
if (!preg_match('/^[A-Za-z](\w|\.)*$/', $controller)) {
throw new HttpException(404, 'controller not exists:' . $controller);
}