前言:

这一版的变化比上一版有点大,首先整个代码的构架被修改了,更加的直观,方便修改和定位。另外,在这一版里我修复了上一版两处BUG(后文会说),添加了伪静态检测XSS(URL处的路径及文件)。

0×01 网站结构:

我把之前写的xss.html修改为getXSS.html。增加了formXSS.html来接受form表单的xss(下一章会专门来说这个功能)。把日志放在了logs文件里。如下:

修改log日志文件的话,nginx的配置文件也要改的。下面是nginx配置后的nginx代码:

location /getXSS.html{
    access_log E:/WWW/xss/logs/getXSS.log xss;
}
location /formXSS.html{
    access_log E:/WWW/xss/logs/formXSS.log xss;
}

下面的ajax代码我就使用logs/xxx.html为接受地址了。

0×02 代码的构架:

因为在写的时候,想把这个插件不止光做成“网站XSS漏洞检测”,以后再添加一些其他的漏洞检测脚本。而且添加了判断,使得在应该的时候再运行脚本,比如当前的网页URL没有参数,那就不运行URL参数XSS漏洞检测脚本,也可以大大的提升脚本的运行效率。于是我把几个常用的变量写在外面,函数里面就可以直接调用,不用再声明(JavaScript变量声明提升),然后在声明的下面使用if判断使用哪些脚本。再把每一个功能都封装成一个函数。

构架如下:

变量声明
变量操作(在if前面,防止if直接跳到函数里,而没有运行)
if判断(跳到哪个函数)
功能函数

变量声明

var onlyString = 'woainixss<>'; //唯一标识符
var protocol = window.location.protocol;  //网站使用的网络协议(http、https等)
var host = window.location.host;   //网站的主域名(*.com、*.cn等,例:test.cn)
var href = window.location.href;   //网站的完整URL(协议+域名+参数+锚)
var hostPath; //用来存放网站除去参数的字符串(协议+域名+锚)
var urlPath;  //用来存放网站URL路径的数组(例:test.cn/test/xss/123  urlPath = ['test','xss’,'123'])

变量操作:

if(href.indexOf("?") != "-1"){  //如果url存在?字符串(存在?基本就存在参数了)
    hostPath = href.slice(0,href.indexOf("?"));      //去除参数,只留下“协议+域名+锚”
}else{
    hostPath = href;  //不存在?则把完整的url赋值给hostPath。
}
urlPath = hostPath.split("/").splice(3);      //以“/”为分隔符,把路径分割成数组。

因为协议后面会有两个//,而.com等域名后面还会有一个/,这些都是要抛弃的。只留下路径。反馈如下:

因为多余的“/”存在,会多出3个数组,使用splice(3)去除就行了。

If判断:

if(location.search != ""){  //当参数不为空时,跳转到parameter_Xss函数里
    parameter_Xss();
}
if(href.split("/")[3] != ""){      //当完整的URL里第三个/后存在字符串,则跳转到pseudoStatic_Xss函数里
    pseudoStatic_Xss();
}
if($("form").length > 0){       //当页面存在form表单,就跳转到form_Xss函数里
    form_Xss();
}

功能函数:

function parameter_Xss(){...}      //URL参数检测XSS
function pseudoStatic_Xss(){...}       //伪静态检测XSS
function form_Xss(){...}     //form表单检测XSS

0×03:URL参数检测XSS的BUG修改:

第一处BUG是我在测试的时候发现的:也是我的疏忽。我在调试代码的时候,没有把各个可能出现的环境考虑到。下面是问题代码:

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变量赋值的时候出现的。我没有考虑到路径的问题。就直接把域名加上参数加上唯一标识符。比如测试的网页是test.cn/test/xss/123?a=1&b=2,但是此时的url就为test.cn/?a=1&b=2。直接把路径给忽略了。这就导致待检测的网页无法正确的被正确的处理。

下面是我修改后的代码:

var url = protocol + "//" + host + "/" + urlPath.join("/") + "?";

至于怕没有路径而导致的报错,大可不必担心。如果不存在路径,urlPath.join("/")则会返回空字符串。

第二处是我第一节时提到的BUG:当网站存在两个参数时,代码会自动把每一个参数的值都加上“唯一标识符符”,然后再交给ajax发送数据。如果这时第一个参数存在XSS,但是第二个参数不能变,否则就无法触发XSS。这个问题在第一版的时候是无法解决的,在这一版就完美的解决了。下面是我重写的代码:

var i; //for循环里的i
var parameter = location.search.substring(1).split("&");    //把URL字符以&分割成字符串
var url = protocol + "//" + host + "/" + urlPath.join("/") + "?";     //拼接成新的URL字符。(例:http://test.cn/test/xss/123?)
for(i = 0;i < parameter.length;i++){      //for循环,有多少个参数就循环多少次
    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"){
            $("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;
}

下面就来说说for语句里的代码的意思。

var parameterData = parameter[i];

parameter[i]代表的是当前参数的字符,如图:

因为下面的操作代码会使当前的parameter[i]值改变,而我们需要再最后再把他改回来,那就需要赋值了,最后的时候我们再输入

parameter[i] = parameterData;

反过来就可以搞定了。

parameter[i] = parameter[i].split("=")[0] + "=" + parameter[i].split("=")[1] + onlyString;

把当前的参数重新定义,parameter[i].split("=")[0]的是获取当的参数的字符串,如下:

parameter[i].split("=")[1]是获取当前参数的值,如下:

onlyString就是“唯一标识符”了,除去ajax,剩下的结果就是下面这样:

拼接URL参数的代码,我放到ajax里了,也就是url + parameter.join("&"),结果如下:

先是第一个参数与“唯一标识符”拼接,然后恢复原样。再把第二个参数与“唯一标识符”拼接。依次下去。

Ajax代码相比上次修改了url:url + parameter.join("&")。然后再加上async:false,让ajax为同步,如果为异步的话,只会取for循环最后一次结果,而改为同步的话,才会正常的循环ajax。其他的就没怎么变。

0×04 伪静态检测XSS:

一开始的时候,我想了很久“如何判断页面是否为伪静态”,网上也有说,是利用document.lastModifie来查看到网页的最后更新时间,如果网页采用的是静态,则日期时间和我们电脑的系统时间不一样。如果网页采用的是伪静态,则日期时间和我们电脑的系统时间完全一致。 但是需要发送两次ajax,来判断时间是否不同,我感觉太浪费效率了。后来我想了下,伪静态的规则一般都是把参数设置为路径,如下:

Test.cn/test.php?id=xss/123

修改后:

Test.cn/test/xss/123。

下面是nginx配置的规则代码:

location /test {
    rewrite ^(/.+)/ /test.php?id=$1 last;
}

那我只需要检测当前网页的路径及文件就行了。所以在if判断的时候,我写的是if(href.split("/")[3] != ""){…}就是来判断当前网页是否存在路径。

var fileURL;  //存放文件名字符串的变量
var fileUrlXss;   //把文件的与“唯一标识符”拼接后的字符
var url;   //ajax发送的url
var xss = "";     //存在xss的参数

我们需要先判断当前url是否存在文件,也就是xxx.html、xxx.php等。如果存在就把xxx与“唯一标识符”拼接,再发送。也就是原本是xxx.html文件,拼接后就成为xxxwoiainixss<>.html了,有人可能会问这有什么用,在伪静态看来,xxx.html只是一个参数,就像freebuf的某一个页面:http://www.freebuf.com/tools/74654.html tools、74654其实就是一个参数而已。并非真的存在tools这个目录和74654.html这个文件。

下面就开始说存在文件的情况下如何修改和发现是否存下XSS。

我们先判断是否存在文件,使用

if(urlPath[urlPath.length-1].indexOf(".") != "-1")

就行了。

urlPath是以/为分隔符而切割的数据,urlPath.length-1则是最后一个数组。indexOf(".")则是判断最后一个数组是否存在“.”字符串,因为如果是文件的话,肯定有“.”的。输出的结果如下:

然后当if为true时,也就是存在文件时,我们就要进行拼接发送数据了。

fileURL = urlPath.pop();    //删除并获取最后一个数组,并赋值给fileURL变量

(其实也就是文件的字符串,例:xxx.html、xxx.php)。

下面就是拼凑字符串了:

fileUrlXss = fileURL.split(".")[0] + onlyString + "." + fileURL.split(".")[1]

fileURL.split(".")[0]是获取文件名(除去后缀)

onlyString 则是“唯一字符串”

fileURL.split(".")[1]是获取文件的后缀字符串。

拼凑的结果就是:

然后交给ajax来发送数据就行了。

发送的url为:protocol + "//" + host + "/" + urlPath.join("/") + "/" + fileUrlXss

协议+域名+路径+文件。完整的代码如下:

$.ajax({
    url: protocol + "//" + host + "/" + urlPath.join("/") + "/" + fileUrlXss,
    type: 'get',
    dataType: 'text',
    async:false,
})
.done(function(data) {
    if(data.indexOf(fileUrlXss) != "-1"){
        xss += fileURL + "|";
    }
})

.done(function(data){…})是当ajax发送成功,目标网站返回200状态码时,执行的代码。Data就是发送数据后的网页HTML源码。然后配上if来判断发送数据后的网页源码里是否存在文件名与“唯一字符串”拼接后的结果。如果存在则把文件名字符串加到xss结尾。结果如下:

只所以使用+=是为了让后面的路径也添加到xss变量里。

下面就说说else里的代码。

fileURL = "";
if(urlPath[urlPath.length-1] == ""){
    urlPath.pop();
}

先把文件的变量给清空,后面字符串拼接的话,也只会与空字符相拼接,并不会造成什么影响。而if里的代码是去掉“/”字符串的。

因为在之前的“变量操作”里,有这样一段代码:

urlPath = hostPath.split("/").splice(3);

把URL以“/”分割成数组,但是这里存在一个BUG,在伪静态里或者正常的链接,经常会有这样的URL:test.cn/test/xss/123/,没有文件,只有路径。导致的结果就是多出一个空数组,如下:

我们需要把最后一个空数组给去掉,但是有的URL是test.cn/test/xss/123这样的,而这样的URL是不存在BUG的,如下:

所以我们需要一个if判断,判断最后一个数组是否为空。如果为空,则剔除最后一个数据,也就是使用pop函数,完整代码为:

urlPath.pop();

OK,文件XSS检测依据OK了,下面就是对URL路径进行检测了。

for(var i = 0;i < urlPath.length;i++){...}

因为这时的urlPath存储的是当前网页URL路径的数组,他的长度就决定了有多少个路径。这里的意思就是循环的次数与路径多少 是一样的。

然后让当前的数组值与“唯一字符串”拼接。

urlPath[i] += onlyString;

拼接后,再与其他变量、字符串拼接:

url = protocol + "//" + host + "/" + urlPath.join("/") + "/" + fileURL;

至于无法复原的情况,加上下面的代码就可以解决了:

urlPath[i] = urlPath[i].substring(0,urlPath[i].length-11);

因为woainixss<>字符是11位,而且在原本字符串的后面,那我们只需要去掉字符串最后11位就可以恢复了。如图:

OK,下面就是在他们之间发送ajax请求了。

因为是在for循环里,所以我们要使用异步执行。也就是async:false。

然后使用if判断是否存在XSS漏洞:

if(data.indexOf(urlPath[i]) != "-1"){
    xss += urlPath[i].substring(0,urlPath[i].length-11) + "|";
}

上面说了,urlPath[i].substring(0,urlPath[i].length-11)是去掉“唯一标识符”后的字符串,把存在的漏洞加到xss变量的结尾,这个时候,最后面的字符一定是|,下面将会把它去掉。

For循环好后,我们在for循环的外面,使用if来判断是否存在XSS。

if(xss == ""){
    return false;
}else{ .... }

当xss为空(不存在XSS漏洞)时,则返回false,来跳出function。else里的代码则是存在XSS时,操作的代码。我们先把最后一个字符串|去掉。

xss = xss.substring(0,xss.length-1);

去掉之后,就是发送字符串到我们的服务端了。

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

这段代码和之前的“URL参数检测XSS”代码是一样的。无需修改,当然了,如果你不想发送,可以使用alert来把结果弹出来。alert(xss)。

完整的代码如下:

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 = "";
        console.log(urlPath)
        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);
        $("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;'>");
    }
}

结尾:

form表单XSS查询,将放到下一章来说,因为这个功能代码比上两个功能的代码总和还多,逻辑也有点复杂,用到了JavaScript里的“闭包”和“filter函数”,再加上for循环里再套一个for循环,还需要重写xss.cn(xss结果反馈网页)。这个功能值得重新开一章节来说。

*本文来自FreeBuf特约作者Black-Hole投稿,属FreeBuf黑客与极客(Freebuf.COM)独家发布,未经允许禁止转载

源链接

Hacking more

...