内容简介:作者:启明星辰ADLab2018年12月9日,ThinkPHP团队发布了一个补丁更新,修复了一处由于路由解析缺陷导致的代码执行漏洞。该漏洞危害程度非常高,默认环境配置即可导致远程代码执行。经过启明星辰ADLab安全研究员对ThinkPHP的56个小版本的源码分析和验证,确定具体受影响的版本为:本地环境采用ThinkPHP 5.1.29+PHP7+Apache进行复现。安装环境后直接访问POC给定的URL即可执行phpinfo(),如图所示:
作者:启明星辰ADLab
公众号: ADLab
漏洞介绍
2018年12月9日,ThinkPHP团队发布了一个补丁更新,修复了一处由于路由解析缺陷导致的代码执行漏洞。该漏洞危害程度非常高,默认环境配置即可导致远程代码执行。经过启明星辰ADLab安全研究员对ThinkPHP的56个小版本的源码分析和验证,确定具体受影响的版本为:
- ThinkPHP 5.0.5-5.0.22
- ThinkPHP 5.1.0-5.1.30
漏洞复现
本地环境采用ThinkPHP 5.1.29+PHP7+Apache进行复现。安装环境后直接访问POC给定的URL即可执行phpinfo(),如图所示:
漏洞分析
以5.1.29版本进行分析,首先看取路由的函数pathinfo:
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’:
当请求报文包含 $_GET['s'] ,就取其值作为pathinfo,并返回pathinfo给调用函数。
分析发现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;
这里是进行路由检测,攻击者可控的$path被传递给了如下的check函数:
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
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'];
}
}
$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()
{}
这里调用parseUrl对 $this->dispatch 进行解析,这是该漏洞的核心点之一:
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分割形成一个数组存到$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给出的模块是index,而实际开发程序不一定存在该模块名,所以构造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函数,继续分析该函数:
public function controller($name, $layer = 'controller', $appendSuffix = false, $empty = '')
{
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,以免遭受攻击。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 漏洞分析:OpenSSH用户枚举漏洞(CVE-2018-15473)分析
- 【漏洞分析】CouchDB漏洞(CVE–2017–12635, CVE–2017–12636)分析
- 【漏洞分析】lighttpd域处理拒绝服务漏洞环境从复现到分析
- 漏洞分析:对CVE-2018-8587(Microsoft Outlook)漏洞的深入分析
- 路由器漏洞挖掘之 DIR-815 栈溢出漏洞分析
- Weblogic IIOP反序列化漏洞(CVE-2020-2551) 漏洞分析
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Introduction to Linear Optimization
Dimitris Bertsimas、John N. Tsitsiklis / Athena Scientific / 1997-02-01 / USD 89.00
"The true merit of this book, however, lies in its pedagogical qualities which are so impressive..." "Throughout the book, the authors make serious efforts to give geometric and intuitive explanations......一起来看看 《Introduction to Linear Optimization》 这本书的介绍吧!