原创作者:FreeBuf特约作者Black-Hole
本章主要是为上三章填坑的(结尾附链接),因为在这期间根据多个用户的意见,发现了不少的bug,因为最近在着手写另一个项目(年底见),没多少时间,但是总要为用户着想。就抽空写了这一章。当然如果光是填坑就没什么意思了。我还会增加几个小功能。想知道是什么?那就看下去吧。
0×01: 当form存在多个img时,会获取第一个,无法遍历下面的img
这个是“form表单检测XSS”功能的bug。是一个基友“温瞳”提出来的。我看了下代码,确实出现了这个问题。因为在第三章搭建测试环境那一小节的时候,我所测试的环境只存在一个img图片,并没有考虑到form表单出现多个图片的问题,这个bug会照成什么危害呢。就是当form表单的一个img是php结尾,但是第二个img是以png结尾,这将导致本次form直接丢掉,不再进行判断筛选。如果一个是png结尾,第二个是php结尾,那程序还会继续向下执行,占用空间也占用时间。
问题就出现这一行formImg = $(index).find("img").attr("src");
因为在jquery里,attr会自动获取第一个DOM的src属性,即使前面是多个DOM。
在这里我重构了tureForm变量。
原本的代码就可以删除了,也就是下面的代码:
接下来就要走向重构之路了。
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顺序了。切记,顺序不能搞乱。
imgArray.push($(imgSrc).attr("src"));
OK,让我们看下现在的返回值吧。
OK,现在已经压入数组了。接下来就是判断imgArray的长度是否大于0了,如果大于0,说明当前的form表单存在img图片。如果不大于0则不存在img图片。
if(imgArray.length > 0){ #存在img图片所运行的code... }else{ #不存在img图片所运行的code... }
现在我们先写当存在img图片时运行的代码。
for(i = 0;i < imgArray.length;i++){ #code... }
然后就是截取src地址,只保留后缀了
if(imgArray[i].indexOf("?") != "-1"){ imgArray[i] = imgArray[i].slice(0,imgArray[i].indexOf("?")); } formImg = formImg.substr(formImg.lastIndexOf("."),formImg.length);
这段代码和之前的代码除了变量名不一样,其他地方都是一样的。把formImg变量改为imgArray[i]就行了。
接下来就是判断后缀了。
if((imgArray[i] != ".png")&&(imgArray[i] != ".jpg")&&(imgArray[i] != ".jpeg")&&(imgArray[i] != ".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[i].indexOf("?") != "-1"){ imgArray[i] = imgArray[i].slice(0,imgArray[i].indexOf("?")); } imgArray[i] = imgArray[i].substr(imgArray[i].lastIndexOf("."),imgArray[i].length); if((imgArray[i] != ".png")&&(imgArray[i] != ".jpg")&&(imgArray[i] != ".jpeg")&&(imgArray[i] != ".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 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");
我们来看下。实际的输出:
我们可以看到返回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... }
我们先来看看现在的输出:
接下来就是if判断当前的input是否存在name属性值了。
if(inputName[i].getAttribute("name")){ return true; })
getAttribute函数是JavaScript原生的方法。和jquery里的attr 函数效果是一样的。如果没有找到name值,那么会隐性返回undefined,而undefined在if里代表的是false。
因为外部函数使用的filter,所以我们只需要返回true就可以了。
上面这个if 是为了让大家了解的。之前也说过return也可以当做if来使用。在实际上可以使用下面的代码:
return (inputName[i].getAttribute("name"));
效果是一样的。完整的代码如下:
tureForm = $(tureForm).filter(function(item,index){ var inputName = $(index).find(":input:not(:submit)"); for(i = 0;i < inputName.length;i++){ return (inputName[i].getAttribute("name")); } })
0×03:添加白名单
这个是在FreeBuf评论区的“冰海”提出来的建议。这个功能也确实应该补上。
因为之前的代码是在window全局里。无法使用return false;(return只能在function函数里使用)来阻止代码向下执行。
而“白名单”就是需要先判断,当当前域名在“白名单”列表里,就不再向下执行。这点在window全局里是无法使用return false;实现的。那就只用使用类似:
throw new Error('xxxx');
这种直接抛出错误,来让浏览器不再向下执行的hack方法了。以为你文章是面对新手,我就不使用这种hack方法了。
先在之前写JavaScript代码外部,添加匿名函数:
(function(){ #之前写的JavaScript代码 })();
如下:
先新建一个变量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[i].indexOf(host) != "-1"){ return false; }
这里为什么要使用indexOf,而不使用if(urlWhiteList[i] == host)呢。
因为这样的话,可以使白名单里的域名,无论二级域名还是三级域名,都可以存在白名单,而不用每一个二级域名、三级域名都要添加到白名单里。当然如果你不想使用这个方法,可以使用我上面说的代码:
if(urlWhiteList[i] == host)替换掉if(urlWhiteList[i].indexOf(host) != "-1"),就行了。完整代码如下: var urlWhiteList = ['baidu.com','360.cn','google.com','xss.cn']; for(var i = 0;i < urlWhiteList.length;i++){ if(urlWhiteList[i].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[i]).find("input").eq(xss - 1).css("border"," 3px solid red");
但是还是不怎么直观,于是我就添加了一段代码,使value值改变。代码如下:
$(tureForm[i]).find("input").eq(xss - 1).css("border"," 3px solid red") .val("此输入框存在XSS ");
下面是实际页面的反馈:
怎么样,是不是比之前更加直观了呢。
其他问题与解答:
Q:有些img的src地址是乱写的。程序能准确的判别出来么?
A:可以的,在这里我们先假设下,当前的网页form表单里存在这样一段img标签代码:
<img src="testtesttest" alt="" />
在0×01节的重构代码中,有这样一段:代码
imgArray[i] = imgArray[i].substr(imgArray[i].lastIndexOf("."),imgArray[i].length);
如果src等于testtesttest,那imgArray[i]就等于testtesttest字符串了。我们现在再来看下,进一步的代码:
"testtesttest".substr("testtesttest".lastIndexOf("."),"testtesttest".length);
反馈如下:
为什么会这样呢,因为lastIndexOf搜索不到字符串的时候,会返回-1,而"testtesttest".length又是testtesttest字符串的长度,subsyr是截取。导致返回testtesttest字符串的最后一个字符t。而在下面的代码里。是判断是否为png、jpg、jphe、gif:
if((imgArray[i] != ".png")&&(imgArray[i] != ".jpg")&&(imgArray[i] != ".jpeg")&&(imgArray[i] != ".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[i].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("/")[3] != ""){ 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[i]; parameter[i] = parameter[i].split("=")[0] + "=" + parameter[i].split("=")[1] + onlyString; $.ajax({ url: url + parameter.join("&"), type: 'get', dataType: 'text', async:false, }) .done(function(data) { if(data.indexOf(parameter[i].split("=")[1]) != "-1"){ alert("当前URL参数" + parameter[i].split("=")[0] + "存在XSS漏洞"); // $("body").append("<img src='http://xss.cn/getXSS.html?host=$" + host + "&$xss=$" + parameter[i].split("=")[0] + "&$url=$" + window.location.href + "&$rand=$" + Date.parse(new Date()) + "' style='display:none;'>"); } }) parameter[i] = parameterData; } } function pseudoStatic_Xss(){ //伪静态检测XSS var fileURL; var fileUrlXss; var url; var xss = ""; if(urlPath[urlPath.length-1].indexOf(".") != "-1"){ fileURL = urlPath.pop(); fileUrlXss = fileURL.split(".")[0] + onlyString + "." + fileURL.split(".")[1] $.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.length-1] == ""){ urlPath.pop(); } } for(var i = 0;i < urlPath.length;i++){ urlPath[i] += onlyString; url = protocol + "//" + host + "/" + urlPath.join("/") + "/" + fileURL; $.ajax({ url: url, type: 'post', dataType: 'text', async:false, }) .done(function(data){ if(data.indexOf(urlPath[i]) != "-1"){ xss += urlPath[i].substring(0,urlPath[i].length-11) + "|"; } }) urlPath[i] = urlPath[i].substring(0,urlPath[i].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[i].indexOf("?") != "-1"){ imgArray[i] = imgArray[i].slice(0,imgArray[i].indexOf("?")); } imgArray[i] = imgArray[i].substr(imgArray[i].lastIndexOf("."),imgArray[i].length); if((imgArray[i] != ".png")&&(imgArray[i] != ".jpg")&&(imgArray[i] != ".jpeg")&&(imgArray[i] != ".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[i].getAttribute("name")); } }) if(tureForm.length <= 0){ return false; } for(i = 0;i < tureForm.length;i++){ actionUrl = $(tureForm[i]).attr("action"); methodType = $(tureForm[i]).attr("method"); if(actionUrl == undefined || actionUrl == "#" || actionUrl == ""){ actionUrl = href; } if(methodType == undefined || methodType == "#"){ methodType = "get"; } tureInput = $(tureForm[i]).find("input:not(:submit)").length for(j = 0;j < tureInput;j++){ sendData += $(tureForm[i]).find("input:not(:submit)")[j].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[i]).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
结语:
在修改代码的时候,我发现自己还是有很多的不足,代码不够模块化,导致修改一处,而牵动全身。本来是想添加几个功能的“ajax xss检测”“IP xss检测”“user-agent xss检测”“数据包头 XSS检测”但是时间真的不够。因为在着手写另一个项目,如果这个“自动检测XSS插件”的牛逼指数算1的话,那我现在写的项目,就是10+,敬请期待。
至于“ajax xss检测”、“IP xss检测”、“user-agent xss检测”、“数据包头 XSS检测”等功能可能要明年开始写或者不再添加。这一章就算是“打造一个自动检测页面是否存在XSS的插件”系列文章的最后一章了。
《打造一个自动检测页面是否存在XSS的插件》系列文章链接:1/2/3。
结局福利:下个月我会做一个送书小活动(《解密家用路由器0day漏洞挖掘技术》、《模糊测试强制发掘安全漏洞的利器》、《爱上单片机》、《android开发实战》、《Web之困》等),最低八成新(邮费自付哦~),到时候会在FreeBuf小酒馆发起,请多多关注。
*本文来自FreeBuf特约作者Black-Hole投稿,属FreeBuf黑客与极客(Freebuf.COM)独家发布,未经允许禁止转载