打造一个自动检测页面是否存在XSS的插件(完结篇)
0×01: 当form存在多个img时,会获取第一个,无法遍历下面的img这个是“form表单检测XSS”功能的bug。是一个基友“温瞳”提出来的。我看了下代码,确实出现了这个问题。因为在第三章搭建测试环境那一小节的时候,我所测试的环境只存在一个img图片,并没有考虑到form表单出现多个图片的问题,这个bug会造成什么危害呢。就是当form表单的一个img是php结尾,但是第二个img是以png结尾,这将导致本次form直接丢掉,不再进行判断筛选。如果一个是png结尾,第二个是php结尾,那程序还会继续向下执行,占用空间也占用时间。
我们来看下代码:
A1.png
问题就出现这一行formImg = $(index).find("img").attr("src");
因为在jquery里,attr会自动获取第一个DOM的src属性,即使前面是多个DOM。
在这里我重构了tureForm变量。
原本的代码就可以删除了,也就是下面的代码:
A2.png
接下来就要走向重构之路了。
104行代码没有变,还是:
tureForm = $("form").filter(function(item,index){#code...})
首先我们先新建一个变量,用于储存img标签里src属性值的后缀。
var imgArray = [];
接下来就是把当前的img标签储存到imgArray里。这里使用了map函数,用来循环对象里的每一个键值。
$(index).find("img").map(function(number,imgSrc){
#code...
});
还记得我在第三章说的,当every、filter、forEach、map、some这些函数的处理对象时Object对象,而不是Array数组时,传入函数里的第一个函数和第二个函数会调换么?这里的$(index).find("img")就是一个对象,所以在传个处理函数的时候,使用的是number,imgSrc。如果是数组的话,就是imgSrc,number顺序了。切记,顺序不能搞乱。
然后就是把src值压入imgArray数组:
imgArray.push($(imgSrc).attr("src"));
OK,让我们看下现在的返回值吧。
A3.png
A4.png
OK,现在已经压入数组了。接下来就是判断imgArray的长度是否大于0了,如果大于0,说明当前的form表单存在img图片。如果不大于0则不存在img图片。
if(imgArray.length > 0){
#存在img图片所运行的code...
}else{
#不存在img图片所运行的code...
}
现在我们先写当存在img图片时运行的代码。
先对imgArray循环:
for(i = 0;i < imgArray.length;i++){
#code...
}
然后就是截取src地址,只保留后缀了
if(imgArray.indexOf("?") != "-1"){
imgArray = imgArray.slice(0,imgArray.indexOf("?"));
}
formImg = formImg.substr(formImg.lastIndexOf("."),formImg.length);
这段代码和之前的代码除了变量名不一样,其他地方都是一样的。把formImg变量改为imgArray就行了。
接下来就是判断后缀了。
if((imgArray != ".png")&&(imgArray != ".jpg")&&(imgArray != ".jpeg")&&(imgArray != ".gif")){
return false;
}else{
return ($(index).prepend().find(":input:not(:submit)").length > 0);
}
当当前的form表单存在img标签,而且后缀不为png、jpg、jpeg、gif时就返回false,也就是说当前的form表单被丢弃了。如果后缀符合png、jpg、jpeg、gif其中一个。则再进行判断,当前form表单除了type=submit之外所有的input标签的长度是否大于0,
($(index).find(":input:not(:submit)").length > 0)这段代码的字符、效率都比原来好很多。原来的代码是:
$(index).find(":submit").length > 0 && ($(index).find(":text").length > 0 || $(index).find(":password").length || $(index).find(":radio").length > 0 || $(index).find(":checkbox").length > 0)
显而易见。这次的代码比原来的代码好很多。而且比原来的代码准确率也高了很多。
因为index是当前的form表单DOM,我们使用find函数搜索当前form表单下符合选择器条件的DOM,find(":input:not(:submit)")意思就是说检索当前form表单里所有的input标签,并且除了type为submit的input标签。
OK,现在这个bug已经完全修复了。下面是完整的代码:
tureForm = $("form").filter(function(item,index){
var imgArray = [];
$(index).find("img").map(function(number,imgSrc){
imgArray.push($(imgSrc).attr("src"));
});
if(imgArray.length > 0){
for(i = 0;i < imgArray.length;i++){
if(imgArray.indexOf("?") != "-1"){
imgArray = imgArray.slice(0,imgArray.indexOf("?"));
}
imgArray = imgArray.substr(imgArray.lastIndexOf("."),imgArray.length);
if((imgArray != ".png")&&(imgArray != ".jpg")&&(imgArray != ".jpeg")&&(imgArray != ".gif")){
return false;
}else{
return ($(index).find(":input:not(:submit)").length > 0);
}
}
}else{
return ($(index).find(":input:not(:submit)").length > 0);
}
})
其他地方不需要再改动了。
0×02:在第一次筛选后,只能获取第一个input的name值
这个bug是我在实际操作中遇到的。接下来就来说说这个bug是多么可恶又是怎么死亡的吧。
这段代码出现在第121行(未修改版本,修改上一个bug后,行数在128行)
让我们看看这段代码长什么样子吧:
tureForm = $(tureForm).filter(function(item,index){
return (!!$(index).find(":input").attr("name"));
})
和上一个bug的情况是一样的。都是因为attr会自动获取第一个DOM的name属性。
让我们先来看看这段代码导致危害性吧。
如果当前页面有一个form表单是下面这样的:
<form method="post">
<input type="text">
<input type="text" name="xss">
<input type="submit">
</form>
那上面这个存在bug的代码,会直接把这个form表单丢弃,而不再做任何的操作与判断。准确性将大大降低。甚至比一个0×01节的bug还要严重的多。
为什么会这个form表单会被直接丢弃呢,我们来深入的了解下。
因为这一个form没有img,也存在除submit之外其他的input的标签。符合第一次筛选。于是这个form表单成功的进入到了第二个筛选代码处。Filter代码我就不解释了,之前有解释过。我们直接来看return代码:
$(index).find(":input").attr("name")
这里的index就是这个form表单,等价于$("form").find(":input").attr("name");
我们来看下。实际的输出:
AA.png
我们可以看到返回undefined,也就是没有获取到。接下来就是修改这段代码了。
tureForm = $(tureForm).filter(function(item,index){})不变。
先定义一个变量,用来储存input标签里的name属性值:
var inputName = $(index).find(":input:not(:submit)");
以上面的form标签为例,这段代码会返回:
<input type="text">
<input type="text" name="xss">
这两个input标签。然后使用for循环inputName变量。
for(i = 0;i < inputName.length;i++){
#code...
}
我们先来看看现在的输出:
A5.png
A6.png
接下来就是if判断当前的input是否存在name属性值了。
if(inputName.getAttribute("name")){
return true;
})
getAttribute函数是JavaScript原生的方法。和jquery里的attr 函数效果是一样的。如果没有找到name值,那么会隐性返回undefined,而undefined在if里代表的是false。
因为外部函数使用的filter,所以我们只需要返回true就可以了。
上面这个if 是为了让大家了解的。之前也说过return也可以当做if来使用。在实际上可以使用下面的代码:
return (inputName.getAttribute("name"));
效果是一样的。完整的代码如下:
tureForm = $(tureForm).filter(function(item,index){
var inputName = $(index).find(":input:not(:submit)");
for(i = 0;i < inputName.length;i++){
return (inputName.getAttribute("name"));
}
})
0×03:添加白名单
这个是在FreeBuf评论区的“冰海”提出来的建议。这个功能也确实应该补上。
因为之前的代码是在window全局里。无法使用return false;(return只能在function函数里使用)来阻止代码向下执行。
而“白名单”就是需要先判断,当当前域名在“白名单”列表里,就不再向下执行。这点在window全局里是无法使用return false;实现的。那就只用使用类似:
throw new Error('xxxx');
这种直接抛出错误,来让浏览器不再向下执行的hack方法了。以为你文章是面对新手,我就不使用这种hack方法了。
先在之前写JavaScript代码外部,添加匿名函数:
(function(){
#之前写的JavaScript代码
})();
如下:
A7.png
先新建一个变量urlWhiteList,用于存储URL白名单地址。
var urlWhiteList = ['baidu.com','360.cn','google.com'];
默认白名单是baidu.com、360.cn、google.cn。
下面就是for循环这个数组了。
for(var i = 0;i < urlWhiteList.length;i++){
#code...
}
OK了后,就是if判断当前域名是否存在白名单里:
if(urlWhiteList.indexOf(host) != "-1"){
return false;
}
这里为什么要使用indexOf,而不使用if(urlWhiteList == host)呢。
因为这样的话,可以使白名单里的域名,无论二级域名还是三级域名,都可以存在白名单,而不用每一个二级域名、三级域名都要添加到白名单里。当然如果你不想使用这个方法,可以使用我上面说的代码:
if(urlWhiteList == host)替换掉if(urlWhiteList.indexOf(host) != "-1"),就行了。完整代码如下:
var urlWhiteList = ['baidu.com','360.cn','google.com','xss.cn'];
for(var i = 0;i < urlWhiteList.length;i++){
if(urlWhiteList.indexOf(host) != "-1"){
return false;
}
}
因为maxthon(遨游浏览器)没有提供储存用户输入的api接口,所以没有办法像chrome那样直接一个html+input添加。而chrome插件开发我还没有深入了解过他的API接口。所以想要添加白名单,大伙可以把插件下载到本地。解压后,打开base.js。Ctrl+F搜索urlWhiteList关键字,在数组后面添加白名单就行了。添加之后,打包按照就OK了。
Maxthon方法,打开http://bbs.maxthon.cn/forum.php?mod=viewthread&tid=611580
下载 mx-插件系统.zip ,里面的MxPacker可以解压、打包插件。
Chrome方法,请自行百度查询。
其他小修改:
在“form_Xss()”(form表单检测XSS)功能反馈处。直接在input标签里添加value值:
之前是
alert("当前页面action为" + actionUrl + "的form表单第" + xss + "个input存在XSS漏洞");
这个方法不咋样,很不直观。于是我在之前加上了使边框颜色变成红色:
$(tureForm).find("input").eq(xss - 1).css("border"," 3px solid red");
但是还是不怎么直观,于是我就添加了一段代码,使value值改变。代码如下:
$(tureForm).find("input").eq(xss - 1).css("border"," 3px solid red")
.val("此输入框存在XSS ");
A8.png
下面是实际页面的反馈:
A9.png
怎么样,是不是比之前更加直观了呢。
其他问题与解答:
Q:有些img的src地址是乱写的。程序能准确的判别出来么?
A:可以的,在这里我们先假设下,当前的网页form表单里存在这样一段img标签代码:
<img src="testtesttest" alt="" />
在0×01节的重构代码中,有这样一段:代码
imgArray = imgArray.substr(imgArray.lastIndexOf("."),imgArray.length);
如果src等于testtesttest,那imgArray就等于testtesttest字符串了。我们现在再来看下,进一步的代码:
"testtesttest".substr("testtesttest".lastIndexOf("."),"testtesttest".length);
反馈如下:
AB.png
为什么会这样呢,因为lastIndexOf搜索不到字符串的时候,会返回-1,而"testtesttest".length又是testtesttest字符串的长度,subsyr是截取。导致返回testtesttest字符串的最后一个字符t。而在下面的代码里。是判断是否为png、jpg、jphe、gif:
if((imgArray != ".png")&&(imgArray != ".jpg")&&(imgArray != ".jpeg")&&(imgArray != ".gif")){
return false;
}else{
return ($(index).find(":input:not(:submit)").length > 0);
}
显然不是了,那么当的form表单就直接运行else里的代码了。
完整代码:
(function(){
var onlyString = 'woainixss<>"';
var protocol = window.location.protocol;
var host = window.location.host;
var href = window.location.href;
var hostPath;
var urlPath;
var urlWhiteList = ['baidu.com','360.cn','google.com'];
for(var i = 0;i < urlWhiteList.length;i++){
if(urlWhiteList.indexOf(host) != "-1"){
return false;
}
}
if(href.indexOf("?") != "-1"){
hostPath = href.slice(0,href.indexOf("?"));
}else{
hostPath = href;
}
urlPath = hostPath.split("/").splice(3);
if(location.search != ""){
parameter_Xss();
}
if(href.split("/") != ""){
pseudoStatic_Xss();
}
if($("form").length > 0){
form_Xss();
}
function parameter_Xss(){ //URL参数检测XSS
var i;
var parameter = location.search.substring(1).split("&");
var url = protocol + "//" + host + "/" + urlPath.join("/") + "?";
for(i = 0;i < parameter.length;i++){
var parameterData = parameter;
parameter = parameter.split("=") + "=" + parameter.split("=") + onlyString;
$.ajax({
url: url + parameter.join("&"),
type: 'get',
dataType: 'text',
async:false,
})
.done(function(data) {
if(data.indexOf(parameter.split("=")) != "-1"){
alert("当前URL参数" +parameter.split("=") + "存在XSS漏洞");
// $("body").append("<img src='http://xss.cn/getXSS.html?host=$" + host + "&$xss=$" + parameter.split("=") + "&$url=$" + window.location.href + "&$rand=$" + Date.parse(new Date()) + "' style='display:none;'>");
}
})
parameter = parameterData;
}
}
function pseudoStatic_Xss(){ //伪静态检测XSS
var fileURL;
var fileUrlXss;
var url;
var xss = "";
if(urlPath.indexOf(".") != "-1"){
fileURL = urlPath.pop();
fileUrlXss = fileURL.split(".") + onlyString + "." + fileURL.split(".")
$.ajax({
url: protocol + "//" + host + "/" + urlPath.join("/") + "/" + fileUrlXss,
type: 'get',
dataType: 'text',
async:false,
})
.done(function(data) {
if(data.indexOf(fileUrlXss) != "-1"){
xss += fileURL + "|";
}
})
}else{
fileURL = "";
if(urlPath == ""){
urlPath.pop();
}
}
for(var i = 0;i < urlPath.length;i++){
urlPath += onlyString;
url = protocol + "//" + host + "/" + urlPath.join("/") + "/" + fileURL;
$.ajax({
url: url,
type: 'post',
dataType: 'text',
async:false,
})
.done(function(data){
if(data.indexOf(urlPath) != "-1"){
xss += urlPath.substring(0,urlPath.length-11) + "|";
}
})
urlPath = urlPath.substring(0,urlPath.length-11);
}
if(xss == ""){
return false;
}else{
xss = xss.substring(0,xss.length-1);
alert("当前伪静态路径或者文件" + xss + "存在XSS漏洞");
// $("body").append("<img src='http://xss.cn/getXSS.html?host=$" + host + "&$xss=$" + xss + "&$url=$" + window.location.href + "&$rand=$" + Date.parse(new Date()) + "' style='display:none;'>");
}
}
function form_Xss(){ //form表单检测XSS
var tureForm;
var tureInput;
var formImg;
var actionUrl;
var methodType;
var sendData = "";
var sendDataUrl;
var i;
var j;
tureForm = $("form").filter(function(item,index){
var imgArray = [];
$(index).find("img").map(function(number,imgSrc){
imgArray.push($(imgSrc).attr("src"));
});
if(imgArray.length > 0){
for(i = 0;i < imgArray.length;i++){
if(imgArray.indexOf("?") != "-1"){
imgArray = imgArray.slice(0,imgArray.indexOf("?"));
}
imgArray = imgArray.substr(imgArray.lastIndexOf("."),imgArray.length);
if((imgArray != ".png")&&(imgArray != ".jpg")&&(imgArray != ".jpeg")&&(imgArray != ".gif")){
return false;
}else{
return ($(index).find(":input:not(:submit)").length > 0);
}
}
}else{
return ($(index).find(":input:not(:submit)").length > 0);
}
})
if(tureForm.length <= 0){
return false;
}
tureForm = $(tureForm).filter(function(item,index){
var inputName = $(index).find(":input:not(:submit)");
for(i = 0;i < inputName.length;i++){
return (inputName.getAttribute("name"));
}
})
if(tureForm.length <= 0){
return false;
}
for(i = 0;i < tureForm.length;i++){
actionUrl = $(tureForm).attr("action");
methodType = $(tureForm).attr("method");
if(actionUrl == undefined || actionUrl == "#" || actionUrl == ""){
actionUrl = href;
}
if(methodType == undefined || methodType == "#"){
methodType = "get";
}
tureInput = $(tureForm).find("input:not(:submit)").length
for(j = 0;j < tureInput;j++){
sendData += $(tureForm).find("input:not(:submit)").getAttribute("name") + "=" + onlyString + j + "&";
}
sendDataUrl = sendData.substring(0,sendData.length-1);
$.ajax({
url: actionUrl,
type: methodType,
dataType: 'text',
data: sendDataUrl,
async:false,
})
.done(function(data){
var xss = "";
for(j = 0;j < tureInput;j++){
if(data.indexOf(onlyString + j) != "-1"){
xss += j + 1 + "|";
}
}
if(xss == ""){
return false;
}else{
xss = xss.substring(0,xss.length-1);
console.log("当前页面action为" + actionUrl + "的form表单第" + xss + "个input存在XSS漏洞");
$(tureForm).find("input").eq(xss - 1).css("border"," 3px solid red")
.val("此输入框存在XSS ");
// $("body").append("<img src='http://xss.cn/formXSS.html?host=$" + href + "&$xss=$" + xss + "&$url=$" +actionUrl + "&$rand=$" + Date.parse(new Date()) + "' style='display:none;'>");
}
})
}
}
})()
Maxthon、chrome插件下载地址:http://pan.baidu.com/s/1c0375vU
Maxthon插件下载地址:http://extension.maxthon.cn/detail/index.php?view_id=2899
结语:
页:
[1]