所以我就重新开了一章来写,也就是这章。关于代码的构架,请移步到FreeBuf之前文章《 打造一个自动检测页面是否存在XSS的插件Ⅱ 》的第0×01和0×02小节。本章就不涉及代码构架了。
先判断页面是否存在form表单,如果不存在,则不运行相关的检测代码。当页面存在form表单时,则过滤出正确符合条件的form表单,满足下面的条件:
一、Form表单里如果存在img标签,则判断src属性值,查看后缀是是否为jpg、png、jpeg、gif,如果不为这些后缀,说明是“验证码”返回false,因为存在“验证码”的form表单,ajax发送会出错,因为我们不知道获取“验证码”的值。 二、form标签必须存在input的type属性为submit的标签,text、password、radio、checkbox这些必须要有一个存在才可以。
上面筛选的结果只是最初的筛选结果,还需要判断form表单里的input是否存在name值,不然无法构造ajax发送的数据。
然后就是发送了。“form表单XSS检测”主要就是筛选出正确符合条件的麻烦。还有发送时数据的处理也比较麻烦。
根据上面的检测思路,我们需要一个符合思路的测试环境。如下:
一、需要一个form表单里存在图片,而且可以触发XSS。 二、需要一个form表单里存在图片,不可以触发XSS。 三、需要一个form表单里存在以“动态文件”(验证码图片)为后缀的form表单。 四、需要一个没有图片,可以触发XSS的form表单。 五、需要一个没有图片,不可以触发XSS的form表单。
存在图片,而且可以触发XSS:
<form method="post"> <input type="text" name="xss"> <img src="http://image.3001.net/images/new/logo.png" alt=""> <input type="submit"> </form><br><hr>
存在图片,不可以触发XSS:
<form method="post" action="http://baidu.com/"> <input type="text" name="xss"> <img src="http://loudong.360.cn/images/logo.png" alt=""> <input type="submit"> </form><br><hr>
存在以“动态文件”(验证码图片)为后缀的form表单:
<form action="#" method="post"> <input type="text" name="xss"> <img src="http://passport.360.cn/captcha.php?m=create&app=i360&scene=login&userip=ra6YBkHL%2B8G6xqIXmQwvKw%3D%3D&level=default&sign=818d50&r=1441020794&_=1441020960160"> <input type="submit"> </form><br><hr>
没有图片,可以触发XSS的form表单:
<form method="get"> <input type="text" name="xss"> <input type="text" name="caonima"> <input type="submit"> </form><br><hr>
没有图片,不可以触发XSS的form表单:
<form method="get" action="http://360.cn/"> <input type="text" name="asd"> <input type="text" name="wocaonimabi"> <input type="submit"> </form>
效果如下:
光写html代码可不行,没有输出怎么办,下面是PHP输出的代码:
<?php if(isset($_POST['xss'])){ echo $_POST['xss']; } if(isset($_GET['a'])){ echo $_GET['a']; } if(isset($_GET['caonima'])){ echo $_GET['caonima']; } ?>
建立一个名为form_Xss函数function form_Xss(){…}
下面是form_Xss函数里的变量:
var tureForm; //存放符合条件的form表单数据 var tureInput; //存放符合条件的input标签数据 var formImg; //form表单里的img标签里的src属性值后缀 var actionUrl; //form表单的action属性的值 var methodType; //form表单的method属性的值 var sendDataUrl; //发送时的URL var i; //第一层for循环的初始变量 var j; //第二层for循环的初始变量
在0×01节说了那么多的判断,就是要筛选符合正确条件的form表单,这里我们不能一直都用for{if…eles…}来完成,太费时间,效率也不高。幸好在JavaScript里,提供了filter这个函数,这个函数会对数组中的每一项都给定一个函数,返回以ture为结果的数组,例如:
var numbers = [1,2,3,4,5]; filterNubmbers = numbers.filter(function(item,index){ return (item > 3) })
会返回number数组里,所有大于3的数组,然后返回一个新的数组,上面的执行结果如下:
item是数组里的项,index是项在数组里的位置。
这里有个坑,后面会遇到,这里说明一下。
fliter函数是提供给Array的,也就是Array.fliter(function(item,index){…}),但是也可以使用Object。也就是Object.filter(function(item,index){…})
我们需要获取form表单,再进行判断。于是代码就如下:
tureForm = $("form").filter(function(item,index){});
Jquery返回的是“Object对象”这点切记,也就是说什么的filter其实是对象。那item和index就已经互换了。下面的index就是数组里的项,也就是页面中每一个form表单数据。我们打印看下:
接下来就是获取form里面的img标签里的src属性:
formImg = $(index).find("img").attr("src");
forImg现在就等于img标签里src的值了。下面就是做判断是否存src这个属性:
if(!!formImg){ /*当存在src属性时,运行代码*/ }else{ /*当不存在src属性时,运行的代码*/ }
!!的作用将后面的forImg强制转换为布尔类型的数据。方便if判断。
这里我们还需要一个if,因为很多的网站,为了防止浏览器对图片进行本地缓存(防止图片无法第一时间更新)而在图片地址后面加上随机参数。我们现在就使用if来去掉这些随机参数。因为随机数一般都是类似这样:
http://www.freebuf.com/logo.png?rand=012350154453
这里我们就要去掉?rand=012350154453,只留下http://www.freebuf.com/logo.png字符串。代码如下:
if(formImg.indexOf("?") != "-1"){ formImg = formImg.slice(0,formImg.indexOf("?")); }
当存在"?"字符串时,则删除?后面的字符串,留下删除后的字符。如下图:
这样一来,就去掉了随机数,接下来就是获取文件的后缀名了。
formImg = formImg.substr(formImg.lastIndexOf("."),formImg.length);
formImg.lastIndexOf(".")是substr函数的第一个参数,计算formImg字符串从后开始第一个"."的下标位置,为什么不用indexOf呢,因为去掉参数后,后缀也就只有几个字符,而前面则是非常多的字符串。这样一来,速度快一些。获取从最后开始出现的"."字符。而且url地址都会有.。比如freebuf.com或者127.0.0.1这样获取的字符串就不准确了。
formImg = formImg.substr(formImg.lastIndexOf("."),formImg.length);这一段代码就完成了获取后缀的功能,我们开输出看下:
获取后缀后,接下来就是判断后缀是否符合条件了:
if(formImg == ".png" || formImg == ".jpg" || formImg == ".jpeg" || formImg == ".gif"){ /*当符合上面的条件时,运行代码*/ }
||管道符的作用是,当前为false时,再判断后面的。说详细点就是:
先判断获取的后缀是否为png,如果不为png则再判断是否为jpg,如果不为jpg再判断是否为jpeg,如果不为jpeg判断是否为gif。如果都不是,则不运行代码。至于else就不用写了。为什么呢?因为当没有return时,JavaScript默认为false。就像下面这样:
有的人可能会问为什么不判断是否为php、asp后缀呢?原因很简单,光一个php就有php3、php等格式了。而且只需要在服务端配置下。nihao这样的后缀都可以以php的方式解析,所以说我就使用了图片格式来进行判断。
当判断成功后,接下来就是return的事了,其实这里还需要一个判断,就是判断当前的form表单里的input[type]属性的值是否存在text、password、radio、checkbox其中之一,而且必须存在type=submit。不然还是不符合条件。总不能还写一个if,那也太麻烦了。这里我们就使用return来判断。因为filter函数只返回turn组成的数组,利用这个特性,我们就在return里判断。代码如下:
return $(index).find(":submit").length > 0 && ($(index).find(":text").length > 0 || $(index).find(":password").length || $(index).find(":radio").length > 0 || $(index).find(":checkbox").length > 0);
当前的表单必须存在submit,如果不存在则返回false,如果存在则判断后面的表达式是否为turn,因为&&是必须两个表达式都为turn时,才返回turn。也就是说($(index).find(":text").length > 0 || $(index).find(":password").length || $(index).find(":radio").length > 0 || $(index).find(":checkbox").length > 0)这里的表达式,必须有一个符合条件才可以。
return $(index).find(":submit").length > 0 && ($(index).find(":text").length > 0 || $(index).find(":password").length || $(index).find(":radio").length > 0 || $(index).find(":checkbox").length > 0);
就可以搞定了。
现在我们来看下反馈是怎么样的:
这个时候可以清楚的看到,第三个form表单。也就是验证码的那么表单,已经被忽视了。
if(tureForm.length <= 0){ return false; }
当tureForm一个数组都不存在时,则就跳出这个函数体(form_Xss),就不在向下运行代码了。
现在我们还要在判断当前表单的input是否存在name属性,如果不存在则跳出这个函数体,为什么要判断这个呢,因为没有name值的话,ajax无法构造请求,这样无法发送数据,更别谈检测了。代码如下:
tureForm = $(tureForm).filter(function(item,index){ return (!!$(index).find(":input").attr("name")); }) if(tureForm.length <= 0){ return false; }
filter这段代码完成的功能就是,把当前的form表单重新组合,把具有name属性的input标签组成新的form表单,把不存在name属性的标签给剔除掉。
下面的if是判断当前新的form标签里有没有input了,没有input的话,就跳出循环。
完整的代码如下:
tureForm = $("form").filter(function(item,index){ formImg = $(index).find("img").attr("src"); if(!!formImg){ if(formImg.indexOf("?") != "-1"){ formImg = formImg.slice(0,formImg.indexOf("?")); } formImg = formImg.substr(formImg.lastIndexOf("."),formImg.length); if(formImg == ".png" || formImg == ".jpg" || formImg == ".jpeg" || formImg == ".gif"){ return $(index).find(":submit").length > 0 && ($(index).find(":text").length > 0 || $(index).find(":password").length || $(index).find(":radio").length > 0 || $(index).find(":checkbox").length > 0); } }else{ return $(index).find(":submit").length > 0 && ($(index).find(":text").length > 0 || $(index).find(":password").length || $(index).find(":radio").length > 0 || $(index).find(":checkbox").length > 0); } }) if(tureForm.length <= 0){ return false; } tureForm = $(tureForm).filter(function(item,index){ return (!!$(index).find(":input").attr("name")); }) if(tureForm.length <= 0){ return false; }
现在的tureForm包含了具有符合条件的form表单数据的数组,这里我们就可以使用for循环来依次拼凑&发送&反馈了。代码如下:
for(i = 0;i < tureForm.length;i++){ /*tureForm[i]代表当前的form表单*/ }
接下来就是获取当前form表单的action发送地址和method发送模式了。
actionUrl = $(tureForm[i]).attr("action"); methodType = $(tureForm[i]).attr("method");
这里可能又问会问,如果没有action或者method怎么办呢,在说这个之前,我先科普下小知识,当form表单没有action属性时,默认是提交到本页面的,而没有method属性时,默认是以get的请求发送的。知道这些下面的代码就好理解了:
if(actionUrl == undefined || actionUrl == "#" || actionUrl == ""){ actionUrl = href; } if(methodType == undefined || methodType == ""){ methodType = "get"; }
判断是否为undefined是因为当form表单没有action或者method属性时会返回undefined。至于为什么要判断action是否为#,因为#代表的也是本页面。
当不存在action属性或者action为#、空时,action为本页面。method同理。
下面就是去掉当前form表单里的除submit之外所有的input个数:
tureInput = $(tureForm[i]).find("input:not(:submit)").length;
下面就是对这些input的拼凑。这里还需要一个for循环,最外层的for循环是获取当前的form表单,这里的for循环是获取当前form表单里的input标签。
for(j = 0;j < tureInput;j++){ sendData += $(tureForm[i]).find("input:not(:submit)")[j].getAttribute("name") + "=" + onlyString + j + "&"; }
sendData是拼凑后的字符,至于为什么这里不用attr,而是用getAttribute这种原生态JavaScript,因为前面的[j]就已经把前面的jQuery对象转成原生态的JavaScript了,想要使用attr也可以,这样写:
$($(tureForm[i]).find("input:not(:submit)")[j]).getAttribute("name")
只是这样太浪费效率了,jQuery转JavaScript再转jQuery,不值得。
onlyString + j就是当前这个input的唯一标识符了。
当然for循环后肯定会多出一个&,现在我们来把他去掉:
sendDataUrl = sendData.substring(0,sendData.length-1);
因为sendData使用的+=,所以sendDate不能在全部变量里什么,于是我们在for开始的第一行写上var sendData = "";
就可以每一次循环就把sendDate清空。
现在我们来看看反馈:
下面就是利用ajax发送数据了:
$.ajax({ url: actionUrl, type: methodType, dataType: 'text', data: sendDataUrl, async:false, })
Url是action发送地址,type是method发送模式,data就是拼凑的数据。
async:false是以异步发送,防止for+ajax的bug。
当发送成功后,我们需要再在服务端反馈的数据里找到某个form标签里的某个input的唯一标识符。
发送成功的ajax函数是:
.done(function(data){ /*data为发送成功后,服务端的反馈的数据*/ })
先var xss = "";,把存在XSS的数据,存放在里面。
我们就拿这个来进行for循环。
for(j = 0;j < tureInput;j++){ if(data.indexOf(onlyString + j) != "-1"){ xss += j + 1 + "|"; } }
If判断当前form发送的数据里存不存在XSS。这里的onlyString + j和上面拼接的数据是一样的,不然无法判断,当data.indexOf(onlyString + j) != "-1"的结果为true时,说明存在XSS,这个时候我们在把存在XSS的数据,放到xss变量里。
至于为什么要+1,是因为tureInput的长度是从0开始的,如果不+1,那到时候在日志反馈的时候,就是第0个input存在XSS漏洞了。至于后面会多出一个|字符,下面再做操作。
然后if判断当前的form存不存在XSS漏洞,其实也就是判断xss变量是否为空。
if(xss == ""){ return false; }else{ /*存在XSS时,要运行的代码*/ }
当不存在时,跳出本函数,注意这个时候的函数体不是form_Xss()了,而是.done(function(data){}这个函数体。切记。
当存在XSS时,先去掉最后面的|字符。
xss = xss.substring(0,xss.length-1);
然后就是回显了,我这里提供三个回显的代码:
完整的代码如下:
for(i = 0;i < tureForm.length;i++){ var sendData = ""; 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); // $(tureForm[i]).find("input").eq(xss - 1).css("border"," 3px solid red"); // alert("当前页面action为" + actionUrl + "的form表单第" + xss + "个input存在XSS漏洞"); $("body").append("<img src='http://xss.cn/formXSS.html?host=$" + href + "&$xss=$" + xss + "&$url=$" +actionUrl + "&$rand=$" + Date.parse(new Date()) + "' style='display:none;'>"); } }) }
在上一节,我们看到第三种方法的反馈,现在就是重写这种反馈。我们这里就直接说代码了,日志环境的重新搭建,请移步FB文章《 打造一个自动检测页面是否存在XSS的插件Ⅱ 》的第0×01和0×02小节观看。
重新后的代码和直接比,是没有什么太大的变化的,就是添加了“默认隐藏”、“点击打开”、“提醒文字”、“把html移到JavaScript之外,JavaScript只负责数据处理”。
因为默认是隐藏,点击在打开,所以我们需要css代码。如下:
<style> .hide{ display: none; } .show{ display: inline-table !important; } small{ color: #898FFF; } </style>
大体的格式如下:
Script里就是处理数据的,和之前的是没什么太大变化。
<script> $(".panel-heading").next("table").addClass('hide'); $(".panel-heading").click(function() { if($(this).next("table").attr("class") == "table hide"){ $(this).next("table").removeClass('hide'); $(this).next("table").addClass('show'); }else{ $(this).next("table").removeClass('show'); $(this).next("table").addClass('hide'); } }); </script>
完整的的代码如下:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>XSS反馈</title> <script src="http://apps.bdimg.com/libs/jquery/2.1.4/jquery.js"></script> <link rel="stylesheet" type="text/css"href="http://apps.bdimg.com/libs/bootstrap/3.3.4/css/bootstrap.css"> <style> .hide{ display: none; } .show{ display: inline-table !important; } small{ color: #898FFF; } </style> </head> <body> <div class="panel panel-default"> <div>网站URL参数存在XSS漏洞结果</div> <table> <thead> <tr> <th>序列</th> <th>网站域名</th> <th>存在漏洞参数</th> <th>完整的原URL</th> </tr> </thead> <tbody> <script> $.ajax({ url: '/logs/getXSS.log', type: 'get', dataType: 'text', }) .done(function(data) { var host = ""; var xss = ""; var url = ""; var htmlText = ""; data = data.split("/n"); if(data[data.length-1] == ""){ data.pop(); } for(var i = 0;i < data.length;i++){ data[i] = data[i].substring(15); data[i] = data[i].substring(0,data[i].length-11); } for(i = 0;i<data.length;i++){ data[i] = data[i].split("&$"); host += data[i][0].split("=$")[1] + " "; xss += data[i][1].split("=$")[1] + " "; url += data[i][2].split("=$")[1] + " "; } host = host.split(" "); host.pop(); xss = xss.split(" "); xss.pop(); url = url.split(" "); url.pop(); for(i = 0;i < data.length;i++){ htmlText += "<tr><td>"+ (i+1) +"</td><td>" + host[i] + "</td><td>" + xss[i] + "</td><td>" + url[i] + "</td></tr>"; } $($("tbody")[0]).append(htmlText); }) </script> </tbody> </table> </div> <div class="panel panel-default"> <div>网站Form表单存在XSS漏洞结果 <small>“存在漏洞参数”以 | 为分隔符,1代表第一个input,2代表第二个input。input的位置在action地址的Form表单里</small></div> <table> <thead> <tr> <th>序列</th> <th>网站URL</th> <th>存在漏洞参数</th> <th>Form表单的action地址</th> </tr> </thead> <tbody> <script> $.ajax({ url: '/logs/formXSS.log', type: 'get', dataType: 'text', }) .done(function(data) { var host = ""; var xss = ""; var url = ""; var htmlText = ""; data = data.split("/n"); if(data[data.length-1] == ""){ data.pop(); } for(var i = 0;i < data.length;i++){ data[i] = data[i].substring(15); data[i] = data[i].substring(0,data[i].length-11); } for(i = 0;i<data.length;i++){ data[i] = data[i].split("&$"); host += data[i][0].split("=$")[1] + " "; xss += data[i][1].split("=$")[1] + " "; url += data[i][2].split("=$")[1] + " "; } host = host.split(" "); host.pop(); xss = xss.split(" "); xss.pop(); url = url.split(" "); url.pop(); for(i = 0;i < data.length;i++){ htmlText += "<tr><td>"+ (i+1) +"</td><td>" + host[i] + "</td><td>" + xss[i] + "</td><td>" + url[i] + "</td></tr>"; } $($("tbody")[1]).append(htmlText); }) </script> </tbody> </table> </div> <script> $(".panel-heading").next("table").addClass('hide'); $(".panel-heading").click(function() { if($(this).next("table").attr("class") == "table hide"){ $(this).next("table").removeClass('hide'); $(this).next("table").addClass('show'); }else{ $(this).next("table").removeClass('show'); $(this).next("table").addClass('hide'); } }); </script> </body> </html>
现在的页面就是这样:
之前有朋友和我说360也推出了XSS自动检测插件,我也下载安装看了。不能说谁的更好,只能说各有千秋,360的XSS检测插件主要是针对URL的参数进行fuuzing,灵活性比我写的好多了。但是反馈需要提升。同一个参数存在XSS的话,会出现好几个alert弹窗。360的更加趋向于灵活性。我这个更加的趋向于自动化操作。不过360倒是提醒了我平台的重要性,下面是chrome、Maxthon插件下载地址:
合计: http://pan.baidu.com/s/1c0375vU
Chrome:上传插件还要交费,所以我就没上传到chroem应用平台
Maxthon: http://extension.maxthon.cn/detail/index.php?view_id=2899
最后说下,后面会在这个插件的基础上添加“路径扫描”“任意文件读取”等功能。