还记得刚玩Web安全时,就想着要是能有一个自动挖掘XSS漏洞的软件就好了。然后我发现了Safe3、JSky、AWVS、Netsparker等等,但是误报太多,而且特别占内存。后来发现了fiddler的一个插件也可以检测当前浏览的页面是否存在XSS,但是不利于查看。于是就有了本文。
本文将从每段代码,每个步骤讲解“如何自己写一个自动检测网页是否存在XSS”的插件。
我现在是在做前端,学了那么久。教会了我一件事,就是干活之前先构思好、规划好。只需要大体的说下就下,比如这个插件,我先是在本子上写了下面的计划:
[ [参数,值], [参数,值] ]
(后来因发现会使代码臃肿,就放弃了这个想法)
构思,并不是后面代码就必须遵守。只是给出一个可行的方案,然后在这个方案上优化一下。
我现在的网页是 http://test.cn/?a=1&b=2&c=3 ,后面就根据这个URL来进行操作。
为了以后添加新功能,我们就把每一个功能封装成一个函数。防止代码混乱不堪。
一、这个功能是检测当前网页是否存在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 文件:
成功了。
现在的日志不是人看的。简直侮辱了使用者的智商。
下面就是对日志进行格式化的操作。因为一年没碰 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>
现在让我们看看网站是什么样子吧:
接下来就是让它成为一个插件,安装之后,我们只需要偶尔打开下 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 越办越好!
*本文来自FreeBuf特约作者Black-Hole投稿,属FreeBuf黑客与极客(Freebuf.COM)独家发布,未经允许禁止转载