Quantcast
Channel: CodeSection,代码区,网络安全 - CodeSec
Viewing all articles
Browse latest Browse all 12749

ThinkPHP5 远程代码执行漏洞分析

0
0
作者:启明星辰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(),如图所示:


ThinkPHP5 远程代码执行漏洞分析
漏洞分析

以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’:
ThinkPHP5 远程代码执行漏洞分析
当请求报文包含 $_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/thi

Viewing all articles
Browse latest Browse all 12749