打造一个自动检测页面是否存在XSS的插件Ⅲ

0×01 检测思路

先判断页面是否存在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检测”主要就是筛选出正确符合条件的麻烦。还有发送时数据的处理也比较麻烦。

0×02 测试环境

根据上面的检测思路,我们需要一个符合思路的测试环境。如下:

  一、需要一个form表单里存在图片,而且可以触发XSS。  二、需要一个form表单里存在图片,不可以触发XSS。  三、需要一个form表单里存在以“动态文件”(验证码图片)为后缀的form表单。  四、需要一个没有图片,可以触发XSS的form表单。  五、需要一个没有图片,不可以触发XSS的form表单。

测试环境代码如下。

存在图片,而且可以触发XSS:

  <form method="post" action="http://baidu.com/">      <input type="text" name="xss">      <img src=http://www.zhongguocaidao.com/uploadfile/2015/0922/20150922105255912.png" alt="">      <input type="submit">  </form><br><hr>

存在图片,不可以触发XSS:

 

存在以“动态文件”(验证码图片)为后缀的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输出的代码:

 
 

0×03 函数变量

 

建立一个名为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×04 form表单筛选

在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){…})

只是当为Object对象时,item和index会互换。前面说到item是数组里的项,但是当为Object时,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.length则是formImg变量的长度,也就是去掉参数的图片地址有多少个字符串。

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)这里的表达式,必须有一个符合条件才可以。

OK,这个时候存在图片的判断已经写完了,那当前的form表单不存在图片怎么办呢?这个时候我们就不需要那么多的if判断图片格式了。我就直接在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);

就可以搞定了。

现在我们来看下反馈是怎么样的:

 

这个时候可以清楚的看到,第三个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;  }

0×05 发送数据

现在的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");

actionUrl代表了当前form表单的action字符串,methodType同理。

这里可能又问会问,如果没有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循环里,所以我们可以使用tureInput(当前form表单除去submit之外的input个数)变量。

我们就拿这个来进行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);

然后就是回显了,我这里提供三个回显的代码:

一、alert("当前页面action为" + actionUrl + "的form表单第" + xss + "个input存在XSS漏洞"); //直接弹出,因为页面的form表单无法直观的看到哪个是第一个,哪个是第二个,所以,这里就使用action来做为标识符。就像下面这样:

 

 

二、$(tureForm[i]).find("input").eq(xss – 1).css("border"," 3px solid red");直接给存在input的标签外部画上css,看起来直观,但是页面就不怎么好看了,就像下面这样:

 

三、$("body").append("");   //也是我推荐的方式,反馈的结果如下:

 

完整的代码如下:

  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("");          }      })  }

0×06 重写反馈

在上一节,我们看到第三种方法的反馈,现在就是重写这种反馈。我们这里就直接说代码了,日志环境的重新搭建,请移步FB文章《打造一个自动检测页面是否存在XSS的插件Ⅱ》的第0×01和0×02小节观看。

重新后的代码和直接比,是没有什么太大的变化的,就是添加了“默认隐藏”、“点击打开”、“提醒文字”、“把html移到JavaScript之外,JavaScript只负责数据处理”。

因为默认是隐藏,点击在打开,所以我们需要css代码。如下:
 

  <style>      .hide{          display: none;      }      .show{          display: inline-table !important;      }      small{          color: #898FFF;      }  </style>
 
     .hide{          display: none;      }      .show{          display: inline-table !important;      }      small{          color: #898FFF;      }

大体的格式如下:

Script里就是处理数据的,和之前的是没什么太大变化。

这里因为要实现“默认隐藏”和“点击隐藏”,我们在body下面需要再新建一个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>
 
  <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

最后说下,后面会在这个插件的基础上添加“路径扫描”“任意文件读取”等功能。

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>