尊龙凯官网入口ADLab:ThinkPHP5远程代码执行误差剖析
宣布时间 2018-12-112018年12月9日,ThinkPHP团队宣布了一个补丁更新,修复了一处由于路由剖析缺陷导致的代码执行误差。该误差危害水平很是高,默认情形设置即可导致远程代码执行。经由尊龙凯官网入口ADLab清静研究员对ThinkPHP的56个小版本的源码剖析和验证,确定详细受影响的版本为:
ThinkPHP 5.0.5-5.0.22
ThinkPHP 5.1.0-5.1.30误差复现
误差剖析
library/think/Request.php:678
public function pathinfo()
{
if (is_null($this->pathinfo)) {
if (isset($_GET[$this->config['var_pathinfo']])) {
// 判断URL内里是否有兼容模式参数
$pathinfo = $_GET[$this->config['var_pathinfo']];
unset($_GET[$this->config['var_pathinfo']]);
} elseif ($this->isCli()) {
// CLI模式下 index.php module/controller/action/params/...
$pathinfo = isset($_SERVER['argv'][1]) ? $_SERVER['argv'][1] : '';
} elseif ('cli-server' == PHP_SAPI) {
$pathinfo = strpos($this->server('REQUEST_URI'), '?') ? strstr($this->server('REQUEST_URI'), '?', true) : $this->server('REQUEST_URI');
} elseif ($this->server('PATH_INFO')) {
$pathinfo = $this->server('PATH_INFO');
}
// 剖析PATHINFO信息
if (!isset($pathinfo)) {
foreach ($this->config['pathinfo_fetch'] as $type) {
if ($this->server($type)) {
$pathinfo = (0 === strpos($this->server($type), $this->server('SCRIPT_NAME'))) ?
substr($this->server($type), strlen($this->server('SCRIPT_NAME'))) : $this->server($type);
break;
}
}
}
$this->pathinfo = empty($pathinfo) || '/' == $pathinfo ? '' : ltrim($pathinfo, '/');
}
return $this->pathinfo;
}
该路由函数中$this->config['var_pathinfo']是设置文件的默认值,其初始化代码如下,值为’s’:
剖析发明pathinfo函数被library/think/Request.php:716中的path函数挪用:
public function path()
{
if (is_null($this->path)) {
$suffix = $this->config['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源自pathinfo,因此可以被攻击者控制。继续剖析该变量的转达,在library/think/App.php:597中被引用:
//public function routecheck()
$path = $this->request->path();
// 是否强制路由模式
$must = !is_null($this->routeMust) ? $this->routeMust : $this->route->config('url_route_must');
// 路由检测 返回一个Dispatch工具
$dispatch = $this->route->check($path, $must);
if (!empty($routeKey)) {
try {
if ($option) {
$this->cache->connect($option)->tag('route_cache')->set($routeKey, $dispatch);
} else {
$this->cache->tag('route_cache')->set($routeKey, $dispatch);
}
} catch (\Exception $e) {
// 保存闭包的时间缓存无效
}
}
return $dispatch;
public function check($url, $must = false)
{
// 自动检测域名路由
$domain = $this->checkDomain();
$url = str_replace($this->config['pathinfo_depr'], '|', $url);
$completeMatch = $this->config['route_complete_match'];
$result = $domain->check($this->request, $url, $completeMatch);
if (false === $result && !empty($this->cross)) {
// 检测跨域路由
$result = $this->cross->check($this->request, $url, $completeMatch);
}
if (false !== $result) {
// 路由匹配
return $result;
} elseif ($must) {
// 强制路由不匹配则抛出异常
throw new RouteNotFoundException();
}
// 默认路由剖析
return new UrlDispatch($this->request, $this->group, $url, [
'auto_search' => $this->autoSearchController,
]);
}
剖析代码可知,若是开启了强制路由则会抛出异常,也就是官方所说的该误差在开启强制路由的情形下不受影响(默认不开启)。
Check函数最后实例化一个UrlDispatch工具,将$url转达给了却构函数。继续剖析UrlDispatch的父类也就是Dispatch类的结构函数:
library/think/route/Dispatch.php:64
{
$this->request = $request;
$this->rule = $rule;
$this->app = Container::get('app');
$this->dispatch = $dispatch;
$this->param = $param;
$this->code = $code;
if (isset($param['convert'])) {
$this->convert = $param['convert'];
}
}
$dispatch变量可控并赋值给了$this->dispatch,经由多次函数挪用返回,最后如下的Url类的init 函数将会被挪用来处置惩罚$this->dispatch。
class Url extends Dispatch
{
public function init()
{
// 剖析默认的URL规则
$result = $this->parseUrl($this->dispatch);
return (new Module($this->request, $this->rule, $result))->init();
}
public function exec()
{}
protected function parseUrl($url)
{
$depr = $this->rule->getConfig('pathinfo_depr');
$bind = $this->rule->getRouter()->getBind();
if (!empty($bind) && preg_match('/^[a-z]/is', $bind)) {
$bind = str_replace('/', $depr, $bind);
// 若是有�?�/控制器绑定
$url = $bind . ('.' != substr($bind, -1) ? $depr : '') . ltrim($url, $depr);
}
list($path, $var) = $this->rule->parseUrlPath($url);
if (empty($path)) {
这里挪用parseUrlPath函数对$url举行剖析,继续剖析该函数:
public function parseUrlPath($url)
{....
....
} elseif (strpos($url, '/')) {
// [�?�/控制器/操作]
$path = explode('/', $url);
} elseif (false !== strpos($url, '=')) {
// 参数1=值1&参数2=值2...
$path = [];
parse_str($url, $var);
} else {
$path = [$url];
}
return [$path, $var];
}
显然,$url的名堂为“�?�/控制器/操作”,$url支解形成一个数组存到$path变量中并返回到挪用者。
继续剖析封装路由的代码:
library/think/route/dispatch/Url.php:48
list($path, $var) = $this->rule->parseUrlPath($url);...
...
// 剖析�?�
$module = $this->rule->getConfig('app_multi_module') ? array_shift($path) : null;
if ($this->param['auto_search']) {
$controller = $this->autoFindController($module, $path);
} else {
// 剖析控制器
$controller = !empty($path) ? array_shift($path) : null;
}
// 剖析操作
$action = !empty($path) ? array_shift($path) : null;
...
...
// 设置目今请求的参数
$this->request->setRouteVars($var);
// 封装路由
$route = [$module, $controller, $action];
return $route;
路由封装返回到library/think/route/dispatch/Url.php:20
class Url extends Dispatch
{public function init()
{
// 剖析默认的URL规则
$result = $this->parseUrl($this->dispatch);
return (new Module($this->request, $this->rule, $result))->init();
}
$result就是封装好的路由数组,转达给了Module的结构函数。
由于Module也是继续自Dispatch类,直接看Dispatch的结构函数:
public function __construct(Request $request, Rule $rule, $dispatch, $param = [], $code = null)
{
$this->request = $request;
$this->rule = $rule;
$this->app = Container::get('app');
$this->dispatch = $dispatch;
$this->param = $param;
$this->code = $code;
if (isset($param['convert'])) {
$this->convert = $param['convert'];
}
}
$result赋值给了$this->dispatch。然后挪用Module类的init函数:
public function init()
{
parent::init();
$result = $this->dispatch;
if ($this->rule->getConfig('app_multi_module')) {
// 多�?榘才�
$module = strip_tags(strtolower($result[0] ?: $this->rule->getConfig('default_module')));
...
...
} elseif (!in_array($module, $this->rule->getConfig('deny_module_list')) && is_dir($this->app->getAppPath() . $module)) {
$available = true;
}
...
...
// �?槌跏蓟�
if ($module && $available) {
// 初始化�?�
$this->request->setModule($module);
$this->app->init($module);
} else {
throw new HttpException(404, 'module not exists:' . $module);
}
}
// 获取控制器名
$controller = strip_tags($result[1] ?: $this->rule->getConfig('default_controller'));
$this->controller = $convert ? strtolower($controller) : $controller;
// 获取操作名
$this->actionName = strip_tags($result[2] ?: $this->rule->getConfig('default_action'));
// 设置目今请求的控制器、操作
$this->request
->setController(Loader::parseName($this->controller, 1))
->setAction($this->actionName);
return $this;
}
这里保存第一个对$module的判断,需要让$available即是true,这就需要is_dir($this->app->getAppPath() . $module)建设。官方demo给出的�?槭莍ndex,而现实开发程序纷歧定保存该�?槊�,以是结构payload时这里是一个注重点。
知足这个判断条件后,继续剖析后续的控制流会进入如下module的exec函数:library/think/route/dispatch/Module.php:80
public function exec()
{// 监听module_init
$this->app['hook']->listen('module_init');
try {
// 实例化控制器
$instance = $this->app->controller($this->controller,
$this->rule->getConfig('url_controller_layer'),
$this->rule->getConfig('controller_suffix'),
$this->rule->getConfig('empty_controller'));
if ($instance instanceof Controller) {
$instance->registerMiddleware();
}
} catch (ClassNotFoundException $e) {
throw new HttpException(404, 'controller not exists:' . $e->getClass());
}
剖析发明,$this->controller是攻击者可控的,并转达给了如下的controller函数,继续剖析该函数:
{
list($module, $class) = $this->parseModuleAndClass($name, $layer, $appendSuffix);
if (class_exists($class)) {
return $this->__get($class);
} elseif ($empty && class_exists($emptyClass = $this->parseClass($module, $layer, $empty, $appendSuffix))) {
return $this->__get($emptyClass);
}
throw new ClassNotFoundException('class not exists:' . $class, $class);
}
在这里,name是攻击者可控的,并转达给了如下的parseModuleAndClass函数:
protected function parseModuleAndClass($name, $layer, $appendSuffix)
{if (false !== strpos($name, '\\')) {
$class = $name;
$module = $this->request->module();
} else {
if (strpos($name, '/')) {
list($module, $name) = explode('/', $name, 2);
} else {
$module = $this->request->module();
}
$class = $this->parseClass($module, $layer, $name, $appendSuffix);
}
return [$module, $class];
}
剖析发明,当$name保存反斜杠时就直接将$name赋值给$class并返回。显然,攻击者通过控制输入就可以操控类的实例化历程,从而造成代码执行误差。
补丁剖析
在ThinkPHP5.0.23以及5.1.31版本中,增添了对$controller的正则过滤:
导致无法再传入\think\app这种形式的控制器。
结 论
此误差是由于框架对传入的路由参数过滤不严酷,导致攻击者可以操作非预期的控制器类来远程执行代码。进一步剖析发明,某些ThinkPHP版本不受已果真的POC的影响,这是由于该POC缺乏完整性思量。因此,强烈建议用户实时将5.0.x版本升级到5.0.23,将5.1.x版本升级到5.1.31,以免遭受攻击。


京公网安备11010802024551号