前言:
还记得刚玩Web安全时,就想着要是能有一个自动挖掘XSS漏洞的软件就好了。然后我发现了Safe3、JSky、AWVS、Netsparker等等,但是误报太多,而且特别占内存。后来发现了fiddler的一个插件也可以检测当前浏览的页面是否存在XSS,但是不利于查看。于是就有了本文。
本文将从每段代码,每个步骤讲解“如何自己写一个自动检测网页是否存在XSS”的插件。
0×01 构思很重要:
我现在是在做前端,学了那么久。教会了我一件事,就是干活之前先构思好、规划好。只需要大体的说下就下,比如这个插件,我先是在本子上写了下面的计划:
(一)获取当前网站的URL
(二)获取参数,并对其格式化为二维数组,有助于后面的获取,格式如下:
[ [参数,值],
[参数,值] ]
(后来因发现会使代码臃肿,就放弃了这个想法)
(三)对“值”与“唯一标识符”拼接。
(四)Ajax对当前网页的URL发送数据。
(五)返回200状态码后,进行查找“唯一标识符”。
(六)查找成功后,发送到服务器端。
构思,并不是后面代码就必须遵守。只是给出一个可行的方案,然后在这个方案上优化一下。
我现在的网页是http://test.cn/?a=1&b=2&c=3,后面就根据这个URL来进行操作。
0×02先从JavaScript开始:
为了以后添加新功能,我们就把每一个功能封装成一个函数。防止代码混乱不堪。
一、这个功能是检测当前网页是否存在XSS的,我们就起一个function xssCheck(){}函数。
二、我们需要判断当前这个url是否存在参数,如果是http://freebuf.com/这样的url,没有参数就不就运行下面的代码,这里就需要一个if判断。代码如下:
if(location.search == ""){
return false;
}
三、我们需要设置五个变量。如下:
var onlyString = 'woainixss'; //设置“唯一标识符”
var protocol = window.location.protocol; //获取当前URL的协议,如http:或者https:
var hrefHost = window.location.host; //获取当前URL的域名,如www.freebuf.com
var parameter = location.search.substring(1).split("&"); //去掉前面的"?",再把参数以&为分隔符存为数组。
var url = protocol + "//" + hrefHost + "/?"; //拼接一个新的URL。如http://www.freebuf.com/?
下面是chrome返回的结构,方便理解:
四、接下来就是比较重要的一步了,把“唯一标识符”与“参数的值”进行拼接,因为不确定参数的个数,我采用for循环:
for(var i = 0;i < parameter.length;i++){
url += parameter[i].split("=")[0] + "=" + onlyString + parameter[i].split("=")[1] + '"&';
}
parameter.length的长度是为2(包括0),从上图可以看到。这段代码有点难理解,我们直接输出,看下:
说的简单一点就是对parameter里的数组值以=为分隔符重新化为数组。parameter[0].split("=")[0]就是代表就是当前网页URL第一个参数的属性,parameter[0].split("=")[1]就是代表就是当前网页URL第一个参数的值。
然后parameter后面的[i]循环三次(有多少参数,就循环多少次)。
现在的url变量为http://test.cn/?a=woainixss1"&b=woainixss2"&c=woainixss3"&
至于为什么每个参数的值后面都有"一个双引号,是为了确保XSS的准确性,这个小方法,至少能保证检测XSS时的误报率减少到5%。剩下的5%是网站除了没有对"进行过滤,其他都做了过滤。当然这种情况很少很少。如果想把误报率减小到0%的话,可以把
var onlyString = 'woainixss';
改成:
var onlyString = 'onclick\'\\/&#alert()<>"';
这个唯一标识符为的结果为:
你以为这样就OK了么?不。上面的代码存在一个bug,那就是url后面会多出一个"&",从而导致ajax请求要分割时出现一个空数组。如何解决呢?加上下面的代码就可以解决了:
url = url.substring(0,url.length-1);
五、 都OK之后,就到了最重要的地方。发送请求及回馈查找。这个时候就要用到ajax了,有些人会问ajax不是会遇到跨域问题么,这点无须担心,因为你使用ajax请求的url和原本的url都是一样的,所以无需担心跨域问题。那为什么你要写成插件呢?因为你总不能每打开一个请求都输入下代码,有插件的话,就好办多了。这里说下插件的特性:
不受跨域影响、每打开一次页面都会加载你所写的JavaScript代码。
先写出ajax的框架:
$.ajax({
url: url,
type: 'get',
dataType: 'text',
})
.done(function(data) {
/* Code */
})
上面的ajax代码里,url就是我们构造好的url。type是选择发送数据时采用的方法。dataType是选择返回的数据以什么样的方式回馈,这里的text就是html代码。也就是网站的源码。
.done是当发送数据请求成功时执行的代码。function(data)里的data就是ajax请求成功后的源码。
为什么不写.fail(function(data) {….})呢?因为我们这个是检测XSS的,如果返回失败,就说明修改参数后,会导致网站出现非200的状态码,也就是404状态码或者其他的状态码。这个时候就说明参数无XSS。(其实这里还有一个bug,就是假设网站有两个参数,你修改第一个参数的时候会返回404状态码,修改第二个参数的时候会触发XSS。但是我我这个只有一个ajax,导致两个参数乧做了修改,这样的话及时第二个有XSS漏洞,也无法检测到,之所以没写,是不想把代码搞得太复杂,这个月应该会写第二版,把这个bug补上,再加上检测POST XSS漏洞检测。因为有人JavaScript并不是很熟,所以没有写的多么复杂,在此说声抱歉,以后会补上。)
现在就开始在done里写上处理代码:
我们先新建一个变量,来储存出现XSS漏洞的参数var xss = "";为什么要在后面加上=""呢?因为下面会使用+=运算符,不加的话会返回NaN(非数字)。
然后就是for循环用indexOf函数查找源码里是否存在“唯一标识符”。代码如下:
for(i = 0;i < parameter.length;i++){
/*indexOf函数*/
}
parameter.length就是当前参数的数量。
然后就是在for里写上indexOf代码了。这里使用if来判断当前“唯一标识符”是否存在当前源码里:
if(data.indexOf(onlyString + parameter[i].split("=")[1] + '"') != "-1"){
xss += parameter[i].split("=")[0] + "|";
}
onlyString + parameter[0].split("=")[1] + '"'是“唯一标识符”与第一个参数的值拼接的结果
data.indexOf(xxx) != "-1" 使用indexOf搜索字符串的时候,如果没搜索到,会返回-1,这里就的意思是,当网站存在“唯一标识符”时运行if里的代码。
xss += parameter[i].split("=")[0] + "|";把出现XSS的参数以|为分割(这里后面会多处一个"|"字符,后面会处理。)假设网站的b和c参数存在XSS,那么会返回"b|c|"。
这里存在一个bug,那就是如果没有XSS漏洞怎么办?下面的代码会解决:
if(xss == ""){
return false;
}else{
/*处理代码*/ }
当没有变量不存在XSS漏洞,返回false。
下面就开始处理上面的bug,和发送代码了:
xss = xss.substring(0,xss.length-1);
//img标签里的src属性,为你的服务器地址。
//如果不想加上远程地址,可以吧下面的代码修改为alert(xss)
$("body").append("<img src='http://xss.cn/xss.html?host=$" + hrefHost + "&$xss=$" + xss + "&$url=" + window.location.href + "&$rand=$" + Date.parse(new Date()) + "' style='display:none;'>")
第一段是去掉xss变量后的最后一个字符串,也就是"|"。
第二段就是发送数据到远程服务器。当然你可以修改为alert(xss),但是如果网站存在XSS,就是弹,太烦人了。
为什么要加$呢,为了让split正确的分割。因为window.location.href里也有参数是&和=,导致split分割时,多分割几个数组。
后面要加随机数,加随机数,加随机数,重要的事情说三遍。调试的时候被这个坑惨了。 刷新页面或者打开页面时,log会无变化,因为这时候的图片已经被浏览器缓存了。
OK,代码已经全部OK,下面的完整的代码:
function xssCheck(){
if(location.search == ""){
return false;
}
var onlyString = 'woainixss';
var protocol = window.location.protocol;
var hrefHost = window.location.host;
var parameter = location.search.substring(1).split("&");
var url = protocol + "//" + hrefHost + "/?";
for(var i = 0;i < parameter.length;i++){
url += parameter[i].split("=")[0] + "=" + onlyString + parameter[i].split("=")[1] + '"&';
}
url = url.substring(0,url.length-1);
$.ajax({
url: url,
type: 'get',
dataType: 'text',
})
.done(function(data) {
var xss = "";
for(i = 0;i < parameter.length;i++){
if(data.indexOf(onlyString + parameter[i].split("=")[1] + '"') != "-1"){
xss += parameter[i].split("=")[0] + "|";
}
}
if(xss == ""){
return false;
}else{
xss = xss.substring(0,xss.length-1);
//img标签里的src属性,为你的服务器地址。
//如果不想加上远程地址,可以吧下面的代码修改为alert(xss)
$("body").append("<img src='http://xss.cn/xss.html?host=$" + hrefHost + "&$xss=$" + xss + "&$url=$" + window.location.href + "&$rand=$" + Date.parse(new Date()) + "' style='display:none;'>");
}
})
}
xssCheck();
0×03配置远程服务器:
因为我已经一年没有碰PHP和MySql了,所以这里就是用JavaScript来完成服务器端的配置。这里使用win7+phpstudy来配置。
打开phpstudy配置“站点域名管理”,添加一个xss.cn域名。
记得重启nginx。
然后打开host文件,把xss.cn指向本地:
127.0.0.1 xss.cn
127.0.0.1 www.xss.cn
然后在在网站目录下,新建一个xss.html。里面内容可以为空。
修改nginx.conf和vhosts.conf文件。
增加一个日志模块,名为xss。
然后在vhosts.conf文件,配置xss文件的动作及添加
access_log E:/WWW/xss/xss.log xss;
加上这段代码:
location /xss.html{
access_log E:/WWW/xss/xss.log xss;
}
然后重启下nginx服务器。就行了,这里我们在index.php里面加上
<?php
echo $_GET['b'];
echo $_GET['c'];
?>
来实验下:
可以看到代码被成功渲染了,这个时候你的xss网站根目录会生成一个xss.log日志文件,我们来看下:
可以看到host是出现XSS漏洞的域名,xss是出现漏洞的XSS参数。url是完整的原始URL。
这个只是我们测试的网页,不来点实战有点过不去。当当网的搜索页面的key存在XSS。URL如下:
http://daphne.dangdang.com/list.html?key=hello&inner_cat=all&sort_type=sort_xlowprice_asc
打开后,在控制台输入我们的代码,然后打开xss.log文件:
成功了。
0×04对日志格式化:
现在的日志不是人看的。简直侮辱了使用者的智商。
下面就是对日志进行格式化的操作。因为一年没碰php了,下面还是使用JavaScript来进行格式化。
在xss.cn的根目录下创建一个index.html文件
在开头我们需要远程调用jquery和bootstrap:
<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">
JavaScript读取xss.log日志文件,再对每条格式化,显现:
使用JavaScript读取本地文件我还真没有做过,网上百度都是再说使用new ActiveXObject来读取,但是ActiveXObject的局限性很大,总不能在IE下打开反馈页面。这里介绍一个我自己想到的一个小技巧,使用ajax。之前生成xss.log文件的时候,我们就把xss.log放到了xss.cn的根目录下,这样一样就可以使用ajax来获取了(只读)。
先创建一个ajax模块:
$.ajax({
url: '/xss.log',
type: 'get',
dataType: 'text',
})
.done(function(data){/* 对xss.log的操作 */})
这里的data就是xss.log的本文。
先创建四个变量,分别是储存域名的host、和储存变量的xss、储蓄完整的原URL的url、储蓄html代码的htmlText:
var host = "";
var xss = "";
var url = "";
var htmlText = '<div class="panel panel-default"><div>网站存在XSS漏洞结果</div><table><thead><tr><th>序列</th><th>网站域名</th><th>存在漏洞参数</th><th>完整的原URL</th></tr></thead><tbody>';
现在需要对日志进行初步的排版,先让我们看下ajax取到xss.log文件的内容:
现在我们要求吧本文转成数组,那么下面一段代码就行:
data = data.split("\n");
现在再来看看:
因为nginx生成日志的时候对对日志重开一个头,也就是xss.log文件的结尾会多出一个空格,防止下次写数据时,写到一行。这里我们就需要对最后一个空数组删除:
if(data[data.length-1] == ""){
data.pop();
}
之所以不能直接用data.pop();,是因为不确定后面是否有回车,因为人工有的时候会不小心删除。
去掉干扰符。因为日志里有着很多不需要的字符。比如GET /和HTTP/1.1,接下来就是把他们删除,因为他们的位置是固定的,我们就不需要用正则:
for(var i = 0;i < data.length;i++){
data[i] = data[i].substring(15); //删除GET /字符
data[i] = data[i].substring(0,data[i].length-11); //删除HTTP/1.1字符
}
data.length是当前xss.log文章的行数。也就是数组里的个数。也是出线XSS漏洞的网站的个数。
里面的两个处理代码不能写到一起,会照成错误,具体原因我也不太清楚。
提取信息,一开始的时候我一直想着是横向取数据,然后一直出错。想了很久,换了纵向取数据就好了,但是代码还是有点难看。大伙别介意:
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="test.cn test.cn daphne.dangdang.com test.cn ";
xss="b|c b key b|c ";
Url="http://test.cn/?a=1&b=2&c=3 http://test.cn/?a=1&b=2&c=3 http://daphne.dangdang.com/list.html?key=hello&inner_cat=all&sort_type=sort_xlowprice_asc http://test.cn/?a=1&b=2&c=3 ";
因为我们在处理时,再后面都加了空格,现在就是把他们分割成数组。这里还有一个需要注意,就是最后面也都有一个空格,转化成数组后,最后面会多出一个空数组,我们只需要把它删除即可:
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>";
}
htmlText += "</tbody></table></div></div>";
$("body").append(htmlText);
第一个for呢,是按照当前有多少存在XSS漏洞网站的个数循环的。 (i+1) 是序号,让它从1开始。host[i]、xss[i]、url[i]是当前数据的域名、参数、原URL。
for下面的代码是为了闭合上面的tbody、table、div等标签。
最下面的一行是为了把内容添加到body里。完整代码如下:
<!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">
</head>
<body>
<script>
$.ajax({
url: '/xss.log',
type: 'get',
dataType: 'text',
})
.done(function(data) {
var host = "";
var xss = "";
var url = "";
var htmlText = '<div class="panel panel-default"><div>网站存在XSS漏洞结果</div><table><thead><tr><th>序列</th><th>网站域名</th><th>存在漏洞参数</th><th>完整的原URL</th></tr></thead><tbody>';
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>";
}
htmlText += "</tbody></table></div></div>";
$("body").append(htmlText);
})
</script>
</body>
</html>
现在让我们看看网站是什么样子吧:
0×05 巴拉巴拉小魔仙,变变变:
接下来就是让它成为一个插件,安装之后,我们只需要偶尔打开下xss.cn网站,就可以看到哪些网站存在XSS漏洞了。不用每打开一个网站就输入一次。
这个Maxthon插件需要4个文件,1个目录。结构如下:
Icons是插件上网logo
Base.js是检测代码,也就是0×02节所说的JavaScript代码,直接复制过去就行了(注意配置服务器,如不想配置,把利用img发包那段代码修改为alert(xss))
Jquery.js是jquery代码,大家都知道
Def.json是插件的配置代码,代码如下:
然后使用maxthon官网提供的MxPacker软件进行压缩,压缩后打开就可以使用。
MxPacker:http://bbs.maxthon.cn/forum.php?mod=viewthread&tid=611580
插件下载地址(没有发送到远程地址,我吧发送数据包的代码修改为了alert(xss)):http://pan.baidu.com/s/1jG4EUJ8
因为鄙人只学了maxthon的插件开发,chrome、火狐等浏览器的插件开发,本人不会。如果有会的,可以参考下我的代码。写出来一个,当然 共享了更好。
题外话
这篇文章发布的日期正好是8月12号,去年的8月12号,正好是我第一次在freebuf发文章,那篇文章是XSS的原理分析与解剖:http://www.freebuf.com/articles/web/40520.html
一年过去了,我从xss开始,从xss结束。从最基础的xss开始分享经验,到现在的全自动化检测XSS并前台显示,大家和我都成长了太多太多,回过头看,曾近的激情和梦想造就了今天的你我,挺美好的一件事。
最后祝Freebuf越办越好!