Bees CMS V4.0代码审计

Bees CMS V4.0代码审计

Gat1ta 1,243 2021-09-14

bees cms是通过PHP+MYSQL开发,多语言系统,内容模块易扩展,模板风格多样化,模板制作简单功能强大,专业SEO优化,后台操作方便,完全可以满足企业网站、外贸网站、事业单位、教育机构、个人网站使用。

人生中第一次完整的代码审计实战,使用的Seay代码审计工具,环境使用的Phpstudy一键部署。

拿到源代码后,首先看一下index.php文件,了解一下系统入口。该文件主要操作就是包含了三个文件,如下所示:

require_once('includes/init.php');
require_once('includes/fun.php');
require_once('includes/lib.php');

init.php文件负责一些初始化工作,主要包括将用户输入的数据进行转义过滤,代码如下:

if (!get_magic_quotes_gpc())    //如果没有设置magic_quotes_gpc则手动过滤参数
{
    if (isset($_REQUEST))
    {
        $_REQUEST  = addsl($_REQUEST);
    }
    $_COOKIE   = addsl($_COOKIE);
  $_POST = addsl($_POST);
  $_GET = addsl($_GET);
}
if (isset($_REQUEST)){$_REQUEST  = fl_value($_REQUEST);}    //过滤sql注入
    $_COOKIE   = fl_value($_COOKIE);
  $_GET = fl_value($_GET);

addsl函数中会将用户输入的参数循环调用addslashes,会将单双引号和反斜杠和空字符进行转义,通过这一轮操作后,字符型注入基本凉凉~不过数值型注入还是可以的。

经过addsl转义之后,又调用了fl_value过滤一下sql注入的一些字段,详细代码如下:

function fl_value($str){
  if(empty($str)){return;}
  return preg_replace('/select|insert | update | and | in | on | left | joins | delete |\%|\=|\/\*|\*|\.\.\/|\.\/| union | from | where | group | into |load_file
|outfile/i','',$str);
}

可以看到过滤了一些sql注入需要用到的关键字,但是这是可以绕过的,比如select可以双写绕过,union可以用“uni union on”绕过,其实这个函数用处不大,主要是addsl。

接下来,get到了第一个漏洞:

漏洞1 变量覆盖:

在经历过转义过滤后还有如下操作:

@extract($_POST);
@extract($_GET);
@extract($_COOKIE);

很明显,该操作有全局变量覆盖的漏洞,可以通过该漏洞覆盖SESSION变量直接进入后台,但是测试的时候没成功,不知道为什么。

以上就是init.php的主要功能,在审计其他文件的时候,如果包含了该文件,字符型注入就可以直接放弃了。

看完这个文件后又看了看fun.php,和lib.php,没发现什么问题后开始审计admin目录。

首先看一下login.php,这是后台登陆的入口。

漏洞2 字符型sql注入:

刚刚看完init.php觉得应该不会有什么sql注入了,结果漏洞就来了~

原因是该文件没有包含init.php,至于原因,咱也不知道~详细代码如下:

@ini_set('session.use_trans_sid', 0);
@ini_set('session.auto_start',    0);
@ini_set('session.use_cookies',   1);
error_reporting(E_ALL & ~E_NOTICE);
$dir_name=str_replace('\\','/',dirname(__FILE__));
$admindir=substr($dir_name,strrpos($dir_name,'/')+1);
define('CMS_PATH',str_replace($admindir,'',$dir_name));
define('INC_PATH',CMS_PATH.'includes/');
define('DATA_PATH',CMS_PATH.'data/');
include(INC_PATH.'fun.php');
include(DATA_PATH.'confing.php');
include(INC_PATH.'mysql.class.php');
if(file_exists(DATA_PATH.'sys_info.php')){
include(DATA_PATH.'sys_info.php');
}
@header("Content-type: text/html; charset=utf-8"); 
$mysql=new mysql(DB_HOST,DB_USER,DB_PASSWORD,DB_NAME,DB_CHARSET,DB_PCONNECT);
session_start();
$s_code=empty($_SESSION['code'])?'':$_SESSION['code'];
$_SESSION['login_in']=empty($_SESSION['login_in'])?'':$_SESSION['login_in'];
$_SESSION['admin']=empty($_SESSION['admin'])?'':$_SESSION['admin'];
if($_SESSION['login_in']&&$_SESSION['admin']){header("location:admin.php");}
$action=empty($_GET['action'])?'login':$_GET['action'];
if($action=='login'){
  global $_sys;
  include('template/admin_login.php');
}
//判断登录
elseif($action=='ck_login'){
  global $submit,$user,$password,$_sys,$code;
  $submit=$_POST['submit'];
  $user=fl_html(fl_value($_POST['user']));
  $password=fl_html(fl_value($_POST['password']));
  $code=$_POST['code'];
  if(!isset($submit)){
    msg('请从登陆页面进入');
  }
  if(empty($user)||empty($password)){
    msg("密码或用户名不能为空");
  }
  if(!empty($_sys['safe_open'])){
    foreach($_sys['safe_open'] as $k=>$v){
    if($v=='3'){
      if($code!=$s_code){msg("验证码不正确!");}
    }
    }
    }
  check_login($user,$password);      //存在sql注入

可以看到,该文件没有包含Init.php,只是调用了fl_value过滤一下sql注入需要的字段,可是前面也说了,这个函数并没有太大的用处,是可以绕过的。然后将用户输入直接带入了check_login函数,该函数没有任何过滤措施:

function check_login($user,$password){
  $rel=$GLOBALS['mysql']->fetch_asc("select id,admin_name,admin_password,admin_purview,is_disable from ".DB_PRE."admin where admin_name='".$user."' limit 0,1");  
  $rel=empty($rel)?'':$rel[0];
  if(empty($rel)){
    msg('不存在该管理用户','login.php');
  }
  $password=md5($password);
  if($password!=$rel['admin_password']){  //存在弱类型比较漏洞,只要不等号两边都是0e开头哈希值将被解析为0.
    msg("输入的密码不正确");
  }
  if($rel['is_disable']){
    msg('该账号已经被锁定,无法登陆');
  }
  
  $_SESSION['admin']=$rel['admin_name'];
  $_SESSION['admin_purview']=$rel['admin_purview'];
  $_SESSION['admin_id']=$rel['id'];
  $_SESSION['admin_time']=time();
  $_SESSION['login_in']=1;
  $_SESSION['login_time']=time();
  $ip=fl_value(get_ip());
  $ip=fl_html($ip);
  $_SESSION['admin_ip']=$ip;
  unset($rel);
  header("location:admin.php");

该函数不光存在sql注入,还存在PHP弱类型比较漏洞,关于弱类型比较在这里就不详细说明了,可以参考https://www.cnblogs.com/Mrsm1th/p/6745532.html。

知道了漏洞原理之后,可以通过如下payload直接登陆后台:

user=1'+uni+union+on+seleselectct+1,'admin','202cb962ac59075b964b07152d234b70',1,'0&password=123
完事之后,开始在admin目录一个一个文件开始审计。

漏洞3 sql注入:

该漏洞存在admin_book.php,在删除留言的时候存在两个数字型sql注入,详细代码如下:

elseif($action=='del'){
  $id=$_GET['id'];
  if(empty($id)){die("<script type=\"text/javascript\">alert('参数发生错误,请重新操作');history.go(-1);</script>");}
  $sql="delete from ".DB_PRE."book where id=".$id;
  $mysql->query($sql);
  msg('删除完成','?lang='.$lang.'&nav='.$admin_nav.'&admin_p_nav='.$admin_p_nav);
}
//删除多选
//存在注入,原因跟上面一样
elseif($action=='del_all'){
  $id=$_POST['all'];
  if(empty($id)){msg('请选择需要删除的内容','?lang='.$lang);}
  foreach($id as $k=>$v){
    $sql="delete from ".DB_PRE."book where id=".$v;
    $mysql->query($sql);
  }
  msg("所选内容已经删除",'?lang='.$lang.'&nav='.$admin_nav.'&admin_p_nav='.$admin_p_nav);
}

漏洞原因主要是由于没有调用intval过滤参数,导致可以通过报错注入或者延迟注入。

漏洞4 sql注入:

该漏洞存在于admin_db.php文件中,数据库查询limit后参数可控,详细代码如下:

elseif($action=='save_back'){
  if(!check_purview('data_backup')){msg('<span style="color:red">操作失败,你的权限不足!</span>');}
  $db = $_POST['db'];
  $init = isset($_POST['init'])?$_POST['init']:0;
  $sql_size = 1048;
  $dir = isset($_GET['dir'])?$_GET['dir']:'';
  //缓存所有表
  if($init){
    if(empty($db)){msg('请选择要备份的表');}
    $str="<?php\n\$table_arr=".var_export($db,true).";\n?>";
    $file=DATA_PATH.'cache/db_cache.php';
    creat_inc($file,$str);
    //创建备份目录
    $dir = 'db'.date(YmdHms,time());
    @mkdir(DATA_PATH.'backup/'.$dir);
  }
  @include(DATA_PATH.'cache/db_cache.php');
  $table_id = isset($_GET['table_id'])?$_GET['table_id']-1:0;
  $r_start = isset($_GET['r_start'])?$_GET['r_start']:0;
  $sql = '';
  $start = isset($r_start)?$r_start:0;
  
  for($i=$table_id;$i<count($table_arr)&& strlen($sql)<$sql_size*1000;$i++){
    $table = $table_arr[$i];
    //当前表的备份小于卷大小
    if(strlen($sql) < $sql_size*1000){
      //备份表
      if(!$start){
        $rel=$GLOBALS['mysql']->fetch_asc("SHOW CREATE TABLE `{$table}` ");
        $sql.="DROP TABLE IF EXISTS `".$table."`;\n";
        $sql.=$rel[0]['Create Table'].";\n";
      }
      //备份数据
      $offset=5;
      while(strlen($sql) < $sql_size*1000){
        $record=$GLOBALS['mysql']->fetch_asc("select*from ".$table." limit {$start},{$offset}");

数据库查询中的start字段可控,但是参考https://www.jianshu.com/p/6c1420a7a7d9的注入方法没成功。

漏洞5 任意文件删除:

该漏洞也存在于admin_db.php文件中,由于删除文件时没有对文件路径进行检查,可以造成目录穿越导致任意文件删除。详细代码如下:

elseif($action=='del'){
  if(!check_purview('data_import')){msg('<span style="color:red">操作失败,你的权限不足!</span>');}
  $fl = $_GET['fl'];
  if(empty($fl)){err('<span style="color:red">参数传递错误,请重新操作</span>');}
  $db_handler=@opendir(DATA_PATH.'backup/'.$fl);
  if($db_handler){
    while(false!==($d_file=readdir($db_handler))){
      @unlink(DATA_PATH.'backup/'.$fl.'/'.$d_file);
    }
  }
  @rmdir(DATA_PATH.'backup/'.$fl);
  msg($fl.'删除成功','?action=import&nav='.$admin_nav.'&admin_p_nav='.$admin_p_nav);
可以看到,fl参数没有任何进行任何检查。


漏洞6 sql注入:

该漏洞存在于admin_flash_ad.php文件,是一个数字型注入,详细代码如下:

elseif($action == 'del_cate')
{
  $id = $_GET['id'];
  if(empty($id)){msg('参数传递错误,请重新操作');}
  if($id=='1'){msg('该分类为固定分类,不能删除');}
  //是否有内容
  $sql = "select count(id) as n from ".DB_PRE."flash_ad where cate_id =".$id;
  $rel=$mysql->fetch_asc($sql);
  if($rel[0]['n']){msg('<span style="color:red">请先删除该分类下的图片</span>');}
  $sql = 'delete from '.DB_PRE.'flash_ad_cate where id='.$id;
  $mysql->query($sql);
  msg('分类成功删除','?action=list_cate'.'&nav=list_flash_cate&admin_p_nav='.$admin_p_nav);
}
echo PW;
function is_sq(){if(!ck_ck()){$sql="SELECT COUNT(id) AS m FROM ".DB_PRE."flash_ad WHERE lang='".$GLOBALS['lang']."'";$rel=$GLOBALS['mysql']->fetch_asc($sql);if($rel[0]['m']>=3){return true;}}}

同样因为没有对数字型参数进行过滤。

漏洞7 任意文件上传,任意目录上传,任意文件删除:

由于这三个漏洞在一个代码块中,就写在一起了,详细代码如下:

elseif($action=='save_edit'){
  $id=intval($_POST['id']);
  if(empty($id)){msg('参数发生错误,请重新操作');}
  $is_thumb=intval($_POST['is_thumb']);
  $thumb_width=intval($_POST['thumb_width']);
  $thumb_width=empty($thumb_width)?$_sys['thump_width']:$thumb_width;
  $thumb_height=intval($_POST['thumb_height']);
  $thumb_height=empty($thumb_height)?$_sys['thumb_height']:$thumb_height;
  $pic_alt=$_POST['pic_alt'];//图片alt
  $pic_thumb=$_POST['pic_thumb'];//图片缩略图
  $pic_thumb = iconv('UTF-8','GBK',$pic_thumb);
  $pic_ext=$_POST['pic_ext'];//图片后缀名
  $file_name=CMS_PATH.$_POST['pic'];//上传图片路径
  $file_name = iconv('UTF-8','GBK',$file_name);
  $pic_name=$_POST['pic_name'];//图片名称
  $pic_name = iconv('UTF-8','GBK',$pic_name);
  $pic_path=$_POST['pic_path'];//图片所在目录
  $pic_cate=$_POST['pic_cate'];//图片类别
  $new_pic=$_FILES['new_pic'];
  $return_thumb='';//缩略图
  if(file_exists(DATA_PATH.'sys_info.php')){include(DATA_PATH.'sys_info.php');}
  //是否重新上传图片
  if(is_uploaded_file($new_pic['tmp_name'])){
    //判断大小
    if($new_pic['size']>$_sys['upload_size']){msg('图片太大,请缩小');}
    //判断格式
    if(!in_array(strtolower($new_pic['type']),array('image/gif','image/jpeg','image/png','image/jpg','image/bmp','image/pjpeg'))){msg('上传图片格式不正确');}  //存在任意文件上传
    //图片信息
    $new_pic_info=pathinfo($new_pic['name']);
    //替换图片
    $new_pic_name=CMS_PATH.$pic_path.$pic_name.'.'.$new_pic_info['extension'];
    //删除原来图片
    //@unlink($file_name);
    //上传图片
    @move_uploaded_file($new_pic['tmp_name'],$new_pic_name);  //任意目录上传
    //对文件重新赋值,方便生成缩略图
    $file_name=$new_pic_name;
    //更新数据库
    $new_pic_sql=",pic_ext='".$new_pic_info['extension']."',pic_size='".$new_pic['size']."'";
  }
  
  if($is_thumb){//开启缩略图
    $file_info=@getimagesize($file_name);
    if(empty($file_info)){msg('图片不存在,操作失败');}
    //删除以前的缩略图
    if($pic_thumb){@unlink(CMS_PATH.'upload/'.$pic_thumb);}    //任意文件删除

首先是任意文件上传,很明显只检查了content-type,最直白的上传漏洞了。

任意目录上传,由于move_uploaded_file的路径参数的一部分可控,所以可以任意目录上传,但是文件后缀无法改变。

任意文件删除,由于删除的文件路径可控,所以可以删除任意文件。这个参数压根不应该由客户端指定。

漏洞8 任意文件上传:

该漏洞存在于admin_pic_upload.php文件,详细代码如下:

if(is_uploaded_file($v)){
    $pic_info['tmp_name']=$v;
    $pic_info['size']=$_FILES['up']['size'][$k];
    $pic_info['type']=$_FILES['up']['type'][$k];
    $pic_info['name']=$_FILES['up']['name'][$k];
    $pic_name_alt=empty($is_alt)?'':$pic_alt[$k];
    $is_up_size = $_sys['upload_size']*1000*1000;
    //任意文件上传
    $value_arr=up_img($pic_info,$is_up_size,array('image/gif','image/jpeg','image/png','image/jpg','image/bmp','image/pjpeg','image/x-png'),$up_is_thumb,$up_thumb_width,$up_thumb_height,$logo=1,$pic_name_alt);
    //处理上传后的图片信息

可以看到调用了up_img上传图片,但是通过传参就可以看出用于校验的type数组明显是content-type,跟上面的任意文件上传一个原理。

漏洞9 任意文件上传:

漏洞存在于upload.php,详细代码如下:

if(isset($_FILES['up'])){
if(is_uploaded_file($_FILES['up']['tmp_name'])){
  if($up_type=='pic'){
    $is_thumb=empty($_POST['thumb'])?0:$_POST['thumb'];
    $thumb_width=empty($_POST['thumb_width'])?$_sys['thump_width']:intval($_POST['thumb_width']);
    $thumb_height=empty($_POST['thumb_height'])?$_sys['thump_height']:intval($_POST['thumb_height']);
    $logo=0;
    $is_up_size = $_sys['upload_size']*1000*1000;
    //任意文件上传
    $value_arr=up_img($_FILES['up'],$is_up_size,array('image/gif','image/jpeg','image/png','image/jpg','image/bmp','image/pjpeg'),$is_thumb,$thumb_width,$thumb_height,$logo);

和之前的任意文件上传一样,没什么好说的。