本文首发自安全脉搏,转载请注明出处。

前言

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

测试环境:

ThinkPHP 5.1 beta+ win10 64bit + wamp

漏洞分析

网上已经有些分析文章了,我就正向分析下这次漏洞过程。不同版本的ThinkPHP调用过程和代码会稍有差异,本文分析的是ThinkPHP 5.1 beta的代码,其他版本的可以类似的分析。

首先程序会加载thinkphp/library/think/App.php ,运行run函数

public function run()
{
// 初始化应用
$this->initialize(); try {
if (defined('BIND_MODULE')) {
// 模块/控制器绑定
BIND_MODULE && $this->route->bindTo(BIND_MODULE);
} elseif ($this->config('app.auto_bind_module')) {
// 入口自动绑定
$name = pathinfo($this->request->baseFile(), PATHINFO_FILENAME);
if ($name && 'index' != $name && is_dir($this->appPath . $name)) {
$this->route->bindTo($name);
}
} $this->request->filter($this->config('app.default_filter')); // 读取默认语言
$this->lang->range($this->config('app.default_lang'));
if ($this->config('app.lang_switch_on')) {
// 开启多语言机制 检测当前语言
$this->lang->detect();
} $this->request->langset($this->lang->range()); // 加载系统语言包
$this->lang->load([
$this->thinkPath . 'lang/' . $this->request->langset() . '.php',
$this->appPath . 'lang/' . $this->request->langset() . '.php',
]); // 获取应用调度信息
$dispatch = $this->dispatch;
if (empty($dispatch)) {
// 进行URL路由检测
$dispatch = $this->routeCheck($this->request);
} // 记录当前调度信息
$this->request->dispatch($dispatch); // 记录路由和请求信息
if ($this->debug) {
$this->log('[ ROUTE ] ' . var_export($this->request->routeinfo(), true));
$this->log('[ HEADER ] ' . var_export($this->request->header(), true));
$this->log('[ PARAM ] ' . var_export($this->request->param(), true));
} // 监听app_begin
$this->hook->listen('app_begin', $dispatch); // 请求缓存检查
$this->request->cache(
$this->config('app.request_cache'),
$this->config('app.request_cache_expire'),
$this->config('app.request_cache_except')
); // 执行调度
$data = $dispatch->run(); } catch (HttpResponseException $exception) {
$data = $exception->getResponse();
} // 输出数据到客户端
if ($data instanceof Response) {
$response = $data;
} elseif (!is_null($data)) {
// 默认自动识别响应输出类型
$isAjax = $this->request->isAjax(); $type = $isAjax ? $this->config('app.default_ajax_return') : $this->config('app.default_return_type');
$response = Response::create($data, $type);
} else {
$response = Response::create();
} // 监听app_end
$this->hook->listen('app_end', $response); return $response;
}

跟进这个路由检测的routeCheck函数

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);
}
}
} $must = !is_null($this->routeMust) ? $this->routeMust : $this->config('app.url_route_must'); // 路由检测(根据路由定义返回不同的URL调度)
return $this->route->check($path, $depr, $must);
}

routeCheck函数又调用了path函数,跟进这里的path函数

在 thinkphp/library/think/Request.php 中定义

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;
}

这里的pathinfo也是在Request.php中定义的

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;
}

分析可知 $this->config->get('var_pathinfo')  默认值是s(var_pathinfo是在config/app.php里硬编码的),所以我们可利用$_GET['s']来传递路由信息。

回到 thinkphp/library/think/App.php , 运行到执行调度

这个就是 thinkphp/library/think/route/dispatch/Module.php 函数run的实例

class Module extends Dispatch
{ public function run()
{ $result = $this->action; if (is_string($result)) { $result = explode('/', $result);
} if ($this->app->config('app.app_multi_module')) { // 多模块部署
$module = strip_tags(strtolower($result[0] ?: $this->app->config('app.default_module'))); $bind = $this->app['route']->getBind(); $available = false; if ($bind && preg_match('/^[a-z]/is', $bind)) { // 绑定模块
list($bindModule) = explode('/', $bind); if (empty($result[0])) { $module = $bindModule; $available = true;
} elseif ($module == $bindModule) { $available = true;
}
} elseif (!in_array($module, $this->app->config('app.deny_module_list')) && is_dir($this->app->getAppPath() . $module)) { $available = true;
} // 模块初始化
if ($module && $available) { // 初始化模块
$this->app['request']->module($module); $this->app->init($module); // 加载当前模块语言包
$this->app['lang']->load($this->app->getAppPath() . $module . '/lang/' . $this->app['request']->langset() . '.php'); // 模块请求缓存检查
$this->app['request']->cache( $this->app->config('app.request_cache'), $this->app->config('app.request_cache_expire'), $this->app->config('app.request_cache_except')
); } else { throw new HttpException(404, 'module not exists:' . $module);
}
} else { // 单一模块部署
$module = ''; $this->app['request']->module($module);
} // 当前模块路径
$this->app->setModulePath($this->app->getAppPath() . ($module ? $module . '/' : '')); // 是否自动转换控制器和操作名
$convert = is_bool($this->caseUrl) ? $this->caseUrl : $this->app->config('app.url_convert'); // 获取控制器名
$controller = strip_tags($result[1] ?: $this->app->config('app.default_controller')); $controller = $convert ? strtolower($controller) : $controller; // 获取操作名
$actionName = strip_tags($result[2] ?: $this->app->config('app.default_action')); $actionName = $convert ? strtolower($actionName) : $actionName; // 设置当前请求的控制器、操作
$this->app['request']->controller(Loader::parseName($controller, 1))->action($actionName); // 监听module_init
$this->app['hook']->listen('module_init', $this->app['request']); // 实例化控制器
$instance = $this->app->controller($controller, $this->app->config('app.url_controller_layer'), $this->app->config('app.controller_suffix'), $this->app->config('app.empty_controller'), false); if (is_null($instance)) { throw new HttpException(404, 'controller not exists:' . Loader::parseName($controller, 1));
} // 获取当前操作名
$action = $actionName . $this->app->config('app.action_suffix'); if (is_callable([$instance, $action])) { // 执行操作方法
$call = [$instance, $action]; // 自动获取请求变量
$vars = $this->app->config('app.url_param_type') ? $this->app['request']->route() : $this->app['request']->param();
} elseif (is_callable([$instance, '_empty'])) { // 空操作
$call = [$instance, '_empty']; $vars = [$actionName];
} else { // 操作不存在
throw new HttpException(404, 'method not exists:' . get_class($instance) . '->' . $action . '()');
} $this->app['hook']->listen('action_begin', $call); return Container::getInstance()->invokeMethod($call, $vars);
}
}

跟进92行的实例化控制器

对应的代码 thinkphp/library/think/App.php

public function controller($name, $layer = 'controller', $appendSuffix = false, $empty = '', $throwException = true)
{ if (false !== strpos($name, '\\')) { $class = $name; $module = $this->request->module();
} else { if (strpos($name, '/')) { list($module, $name) = explode('/', $name);
} else { $module = $this->request->module();
} $class = $this->parseClass($module, $layer, $name, $appendSuffix);
} if (class_exists($class)) { return $this->__get($class);
} elseif ($empty && class_exists($emptyClass = $this->parseClass($module, $layer, $empty, $appendSuffix))) { return $this->__get($emptyClass);
} elseif ($throwException) { throw new ClassNotFoundException('class not exists:' . $class, $class);
}
}

问题出在这

当$name以反斜线\开始时直接将其作为类名。

并且在746行调用__get函数

继续跟进__get,调用container类的make函数

跟进make函数,可以得到这个是实例化一个类。也就是说通过$name变量可以控制类名,并且会实例化这个类。

回到 thinkphp/library/think/route/dispatch/Module.php 的run函数,继续看下面的代码

通过$this->app['request']->param()获取实例化类对应的参数,然后通过invokeMethod 函数动态调用方法(跟进invokeMethod可以知道这边主要是通过反射函数实现动态调用)。至此,ThinkPHP 5 整个路由的过程已讲完,类的实例化与方法调用也已完成。

Exp分析

http://localhost/thinkphp5.1beta/public/index.php?s=index/\think\Container/invokefunction&function=call_user_func&vars[0]=phpinfo&vars[1]=1

通过上面的分析知道通过s就可以获取路由信息。

分析下index/\think\Container/invokefunction&function=call_user_func&vars[0]=phpinfo&vars[1]=1 这个payload

  • index是对应的模块

  • \think\Container 以反斜线开头,这就是我们想要实例化的类

  • invokefunction是想\think\Container类想要调用的方法,

  • function=call_user_func&vars[0]=phpinfo&vars[1]=1是对应invokefunction的参数。

关于如何解析把function=call_user_func&vars[0]=phpinfo&vars[1]=1这些解析成invokefuncition参数的,可以看下Request.php 对应的param函数。

对于选用invokefunction这个函数,是因为这也是个反射函数,可以方便的调用任何函数。

需要注意的是不同版本的TP,对应的文件、类名有些差异。当然还有很多的攻击方式,只要你去文件找到可以实例化的类构造相应的payload就行。

ThinkPHP5 远程命令执行漏洞分析的更多相关文章

  1. ThinkPHP 5.x远程命令执行漏洞分析与复现

    0x00 前言 ThinkPHP官方2018年12月9日发布重要的安全更新,修复了一个严重的远程代码执行漏洞.该更新主要涉及一个安全更新,由于框架对控制器名没有进行足够的检测会导致在没有开启强制路由的 ...

  2. ThinkPHP 5.0远程命令执行漏洞分析与复现

    0x00 前言 ThinkPHP官方2018年12月9日发布重要的安全更新,修复了一个严重的远程代码执行漏洞.该更新主要涉及一个安全更新,由于框架对控制器名没有进行足够的检测会导致在没有开启强制路由的 ...

  3. PHPMailer 远程命令执行漏洞 Writeup

    漏洞概述 1.漏洞简介 PHPMailer 小于5.2.18的版本存在远程代码执行漏洞.成功利用该漏洞后,攻击者可以远程任意代码执行.许多知名的 CMS 例如 Wordpress 等都是使用这个组件来 ...

  4. Apache Tomcat远程命令执行漏洞(CVE-2017-12615) 漏洞利用到入侵检测

    本文作者:i春秋作家——Anythin9 1.漏洞简介 当 Tomcat运行在Windows操作系统时,且启用了HTTP PUT请求方法(例如,将 readonly 初始化参数由默认值设置为 fals ...

  5. Drupal 远程命令执行漏洞(CVE-2018-7600)

    名称: Drupal 远程命令执行漏洞 CVE-ID: CVE-2018-7600 Poc: https://paper.seebug.org/578/ EXPLOIT-DB: https://www ...

  6. ThinkPHP 5.x远程命令执行漏洞复现

    ThinkPHP 5.x远程命令执行漏洞复现 一.漏洞描述 2018年12月10日,ThinkPHP官方发布了安全更新,其中修复了ThinkPHP5框架的一个高危漏洞: https://blog.th ...

  7. Supervisord远程命令执行漏洞(CVE-2017-11610)复现

    Supervisord远程命令执行漏洞(CVE-2017-11610)复现 文章首发在安全客 https://www.anquanke.com/post/id/225451 写在前面 因为工作中遇到了 ...

  8. [转帖]Windows DHCPServer远程代码执行漏洞分析(CVE-2019-0626)

    Windows DHCPServer远程代码执行漏洞分析(CVE-2019-0626) ADLab2019-03-15共23605人围观 ,发现 4 个不明物体安全报告漏洞 https://www.f ...

  9. FlexPaper 2.3.6 远程命令执行漏洞 附Exp

    影响版本:小于FlexPaper 2.3.6的所有版本 FlexPaper (https://www.flowpaper.com) 是一个开源项目,遵循GPL协议,在互联网上非常流行.它为web客户端 ...

随机推荐

  1. style属性css与javascript对照表

    有时候会用javascript来控制标签的style,但js的style属性写法跟css有点不一样,通常是一个单词的写法不变,单词-单词属性会去掉“-”,再把第二个单词的首字母大写,估计是为了与减法运 ...

  2. Elastic Stack 笔记(六)Elasticsearch5.6 搜索详解

    博客地址:http://www.moonxy.com 一.前言 Elasticsearch 主要包含索引过程和搜索过程. 索引过程:一条文档被索引到 Elasticsearch 之后,默认情况下 ES ...

  3. JS基础-全方面掌握继承

    前言 上篇文章详细解析了原型.原型链的相关知识点,这篇文章讲的是和原型链有密切关联的继承,它是前端基础中很重要的一个知识点,它对于代码复用来说非常有用,本篇将详细解析JS中的各种继承方式和优缺点进行, ...

  4. [Vue warn]: Duplicate keys detected: 'area'. This may cause an update error.

    运行vue程序,浏览器报错: 原因:检测到重复的密钥:'area',因为在使用v-for循环绑定的时候,key的值是唯一的,不能相同,否则会出现意想不到的bug 解决办法:v-for时绑定的key唯一

  5. Python网络爬虫实战(五)批量下载B站收藏夹视频

    我们除了爬取文本信息,有的时候还需要爬媒体信息,比如视频图片音乐等.就拿B站来说,我的收藏夹内的视频可能随时会失效,所以把它们下载到本地是非常保险的一件事. 对于这种大量列表型的数据,可以猜测B站收藏 ...

  6. 在vue中使用[provide/inject]实现页面reload

    在vue中实现页面刷新有不同的方法: 如:this.$router.go(0),location.reload()等,但是或多或少会存在问题,如页面会一闪等 所以建议使用[provide/inject ...

  7. 使用Hexo开源博客系统,轻松搭建你的个人博客(2)- 配置篇

    上一章节,我们介绍了Hexo的基础搭建,搭建完大家一定发现,是英文版本的,并且页面有点丑陋.这一章节,就来跟大家介绍Hexo的配置和主题的设置. 站点信息 上一章有跟大家提到过_config.yml这 ...

  8. MySQL 深入理解索引B+树存储 (转载))

    出处:http://blog.codinglabs.org/articles/theory-of-mysql-index.html   摘要 本文以MySQL数据库为研究对象,讨论与数据库索引相关的一 ...

  9. 从零开始入门 K8s | 可观测性:你的应用健康吗?

    作者 | 莫源 阿里巴巴技术专家 一.需求来源 首先来看一下,整个需求的来源:当把应用迁移到 Kubernetes 之后,要如何去保障应用的健康与稳定呢?其实很简单,可以从两个方面来进行增强: 首先是 ...

  10. Spring Boot 2.X(三):使用 Spring MVC + MyBatis + Thymeleaf 开发 web 应用

    前言 Spring MVC 是构建在 Servlet API 上的原生框架,并从一开始就包含在 Spring 框架中.本文主要通过简述 Spring MVC 的架构及分析,并用 Spring Boot ...