自动化检测CSRF

前言:

检测form表单类型的CSRF漏洞和检测form表单类型的XSS漏洞最大的不同就是:XSS需要提交才能检测到,而CSRF只需要分析form表单就行了。

前期的准备工作:

既然要写,那么我们就需要demo来帮我们模拟真实环境的下的情况,而0×00节就说明了,本章只针对于form表单,所以我们的demo也就是各式各样的表单。如下图:

基本上来说网上常见的表单类别都包含了,当然如果你发现有些表单没有加入进去,请说明一下,我将会在下一版中修改。

我们先遍历整个网页上的form表单。代码如下:

outerFor:
for(var i = 0;i < $("form").length;i++){
    var formDom = $("form").eq(i); //formDom代表本次循环的form表单元素
    var imageFileSuffix = ['.jpg','.png','.jpge','.ico','.gif','.bmp']; //图片后缀白名单,用户验证图片是否为验证码
    var placeholderFilterKeyword = ['跳','搜','查','找','登陆','注册','search'];  //无用表单黑名单,用于验证这个form表单有没有用(针对input验证)
    var actionFilterKeyword = ['search','find','login','reg'];   //无用表单黑名单,用于验证这个form表单有没有用(针对form表单验证)
}

至于为什么要加上outerFor:,是因为这只是最外层的for循环,里面还有for循环,为了方便我们在最里层的for循环里跳出最外层的本次循环。在最里层的for循环里我会使用continue outerFor;来跳出最外层for的本次循环。(如果没有看懂,请返回上一行重新看,这很重要)

去除类似搜索、页面跳转等无用的form表单:

首先我们需要假象一下有没有特殊的form表单,比如没有action属性,把请求交给JavaScript来完成。而这种特殊的form表单也很常见,所以这里我就先使用if判断action是否存在:

if(formDom.attr("action") != undefined){
    //当action不为空的时候,进行下一步的操作
}

然后就是使用JavaScript的some函数来对action进行判断,当action里的值满足于我们之前设置的黑名单里的字符串时,就直接pass,使用continue来跳出初始化表达式变量为i的本次循环。转化成代码就是下面这样:

if(formDom.attr("action") != undefined){
    var actionCheck = actionFilterKeyword.some(function(item,index){
        return (formDom.attr("action").toLowerCase().indexOf(item)  != "-1");
    })
    if(actionCheck){
        continue;
    }
}

如果对some函数不明白的,请移步:Some函数详解

而在JavaScript里是严格区分大小写的,所以在上面的代码中我使用了toLowerCase()函数,来把action里的值全部转化成小写,然后在其中搜索之前设置的action黑名单,看是否存在。而对比过程如下:

action的值–search(如果此次比对为true,则不会向下进行比对)

action的值–find

……

其返回的结果是布尔型。在《JavaScript高级程序设计》里是这样说明some函数的: 对数组中的每一项运行给定函数,如果该函数对任意一项返回true,则返回true。

这个时候我们可以看到some前面有一个变量。因为some返回的是布尔型,那么actionCheck变量也是一个布尔型,假设当前这个form表单里的action的值为”/searchArticle.php”。那么就会匹配到黑名单里的search字符串,那么some就会停止向下循环,直接返回true。
如下图:

然后使用if判断actionCheck变量。如果为true,那么就使用continue来跳出当前的循环,不向下运行,直接开始下一个循环。

OK,上面的已经完成对form的action属性过滤了,那么下面的将对input进过白名单过滤。

for(var x = 0;x < formDom.find(":text").length;x++){
    var inputTextCheck;
    var inputText =  formDom.find(":text").eq(x);
    if(inputText.attr("placeholder") == undefined){
        continue;
    }
    inputTextCheck = placeholderFilterKeyword.some(function(item,index){
        return (inputText.attr("placeholder").toLowerCase().indexOf(item)  != "-1");
    })
    if(inputTextCheck){
        continue outerFor;
    }
}

首先使用(“:text”)来遍历当前form表单下所有type为text的input标签。

inputTextCheck变量是为了存放some函数的布尔结果。而inputText变量代表了当前的input标签。

然后使用if判断当前input里的placeholder属性是否存在,如果不存在,则跳出初始化表达式变量为x的本次循环。不向下运行,且对下一个input标签进行之前的操作。如果存在且有值的话,if里的表达式会返回false。则这个if判断不会运行,而是向下运行,而代码:

inputTextCheck = placeholderFilterKeyword.some(function(item,index){
    return (inputText.attr("placeholder").toLowerCase().indexOf(item)  != "-1");
})
if(inputTextCheck){
    continue outerFor;
}

和之前判断action的情况的是一样的,这里就不在阐述了。

去除没有提交按钮的form表单:

为什么要写这个,因为有些form表单不是给用户使用的,他没有提交按钮。对用户来说也是不可见状态。而且也不涉及较为核心的操作,那么我们就需要把这个表单剔除掉。代码如下:

if(formDom.find(":submit").length < 1){
    continue;
}

这段代码较为简单,这里也不在阐述了。

去除具有token的form表单:

大家都知道对于CSRF来说,具有token的form表单基本是可以断定是不存在CSRF漏洞的了,当然排除同页面存在XSS漏洞和CSRF漏洞。

而token,我们应该怎么样发现呢?type为hidden?name包含token?,不不不。这些都不准确,没办法减少误报和扩大结果。那我们应该怎么做呢?判断type为hidden的input标签里的value值的长度是否大于10。

具有token功能的input标签的特殊性:

  1. type为hidden
  2. 为了安全起见,token一般是不会小于10位数的。
  3. 总是以input标签为媒介的方式传输给后端服务器中。

OK,那么我们可以遍历当前form表单下所有type为hidden的input标签,再判断value值是否大于10。如果大于10,说明这个表单很大程度上是具有token验证的表单,将会被程序丢弃。跳出初始化表达式变量为i的本次循环。把上面的话转化成代码就是下面这样:

for(var j = 0;j < formDom.find(":hidden").length;j++){
    if(formDom.find(":hidden").eq(j).val().length > 10){
        continue outerFor;
    }
}

程序不复杂,复杂的思路。所以这里看起来代码其实也了没多少,而且相当的简单。所以这里就不对代码进行阐述了。

去除带有验证码的form表单:

有了之前写自动化检测XSS项目的经验,这里思路就清晰多了。获取img的src属性里的值,判断后缀是否为图片格式。代码如下:

if(formDom.find("img").length > 0){
    var imageCheck;
    for(var z = 0;z < formDom.find("img").length;z++){
        var img = formDom.find("img").eq(z);
        var imgSrc = img.attr("src")
        if(!!imgSrc){
            if(imgSrc.indexOf("?") != "-1"){
                imgSrc = imgSrc.slice(0,imgSrc.indexOf("?"));
            }
            imgSrc = imgSrc.substr(imgSrc.lastIndexOf("."),imgSrc.length);
            imageCheck = imageFileSuffix.some(function(item,index){
                return (imgSrc == item);
            })
            if(!imageCheck){
                continue outerFor;
            }
        }
    }
}

首先使用formDom.find(“img”).length来判断当前的form表单里是否存在图片,如果存在,那么if判断会返回true。进入if判断里面后,首先是一个变量,而这个变量是存放some函数返回的布尔结果的。

然后就是一个for循环,对当前form表单里的img表单进行遍历。而变量img代表了当前的img标签。而imgSrc变量代表了当前img标签里的src。

下面是一段if代码if(!!imgSrc)为什么要这样写呢,是强制把imgSrc变量转成布尔型的,如果当前这个img标签是不存在src属性或没有值的情况下,将会返回false,如果存在src且有值的情况下会返回true。

而下面的代码的是为了剔除?后面的字符串:

if(imgSrc.indexOf("?") != "-1"){
    imgSrc = imgSrc.slice(0,imgSrc.indexOf("?"));
}

为什么要写这样的代x’z码呢?原因很简单,未来防止验证码图片被浏览器缓存,需要再后面跟上问号和随机数字,来达到每刷新一次,就会重新请求这个图片。防止浏览器缓存图片。

而imgSrc = imgSrc.substr(imgSrc.lastIndexOf(“.”),imgSrc.length);这段代码是剔除,除了后缀之外所有的字符串。只保留后缀。举个例子,有段img标签是这样写的:

<img src=”https://wwww.baidu.com/code.php?rand=458711541“>,而运行上面的代码后,结果只有.php了,剩下的字符串已经被剔除掉了。

而下面的some函数,和之前是一样的,不做阐述。只是if里面的表达式里多了一个!取反感叹号。为什么要这样写呢。因为之前的都是黑名单的形式,而这里的白名单的形式,既然是相反的,那么就使用!取反就行了。

其他:

整套代码如下:

outerFor:
for(var i = 0;i < $("form").length;i++){
    var formDom = $("form").eq(i);
    var imageFileSuffix = ['.jpg','.png','.jpge','.ico','.gif','.bmp'];
    var placeholderFilterKeyword = ['跳','搜','查','找','登陆','注册','search'];
    var actionFilterKeyword = ['search','find','login','reg'];
    //去除类似搜索、页面跳转等无用的form表单
    if(formDom.attr("action") != undefined){
        var actionCheck = actionFilterKeyword.some(function(item,index){
            return (formDom.attr("action").toLowerCase().indexOf(item)  != "-1");
        })
        if(actionCheck){
            continue;
        }
    }
    for(var x = 0;x < formDom.find(":text").length;x++){
        var inputTextCheck;
        var inputText =  formDom.find(":text").eq(x);
        if(inputText.attr("placeholder") == undefined){
            continue;
        }
        inputTextCheck = placeholderFilterKeyword.some(function(item,index){
            return (inputText.attr("placeholder").toLowerCase().indexOf(item)  != "-1");
        })
        if(inputTextCheck){
            continue outerFor;
        }
    }
    //去除没有提交按钮的form表单
    if(formDom.find(":submit").length < 1){
        continue;
    }
    //去除具有token的form表单
    for(var j = 0;j < formDom.find(":hidden").length;j++){
        if(formDom.find(":hidden").eq(j).val().length > 10){
            continue outerFor;
        }
    }
    //去除带有验证码的form表单
    if(formDom.find("img").length > 0){
        var imageCheck;
        for(var z = 0;z < formDom.find("img").length;z++){
            var img = formDom.find("img").eq(z);
            var imgSrc = img.attr("src")
            if(!!imgSrc){
                if(imgSrc.indexOf("?") != "-1"){
                    imgSrc = imgSrc.slice(0,imgSrc.indexOf("?"));
                }
                imgSrc = imgSrc.substr(imgSrc.lastIndexOf("."),imgSrc.length);
                imageCheck = imageFileSuffix.some(function(item,index){
                    return (imgSrc == item);
                })
                if(!imageCheck){
                    continue outerFor;
                }
            }
        }
    }
    console.log(formDom)
}

这里的console.log(formDom)可以改为ajax等方式发包,或者alert直接提醒此页面可能具有csrf漏洞。至于如何使用,需要大伙手工打包成浏览器插件的形式。而这里我为大家附上我之前写的自动化检测XSS的插件:网盘。大家可以直接解包,修改里面的JavaScript代码为上面完整的代码,再重新打包就行了。

文章呢,还有很多地方不足。而这套程序还只能说是雏形,所以我没有附上直接利用的工具给大家,也是第一次这样。而且有很多地方没有考虑到,比如JSON Hijacking检测。当然下一章会完成的,也会放出可以直接利用的工具。第二章或者第三章可能会把之前写的XSS自动化检测与本章所说的自动化检测CSRF相结合起来。毕竟XSS+CSRF的危害是非常大的。

在我的认知范围内,这是首款检测Referer的工具(不知廉耻的笑了)。今天发现腾讯在2013年就做了类似的产品(这就尴尬了..),不过还好。而且思路和实现方法有所区别。本章说检测Referer、优化token检测机制。而且这些是腾讯产品所没有的撒。

0×01 一些小的变化

之前的黑白名单列表

var placeholderFilterKeyword = ['跳','搜','查','找','登陆','注册','search'];  //无用表单黑名单,用于验证这个form表单有没有用(针对input验证)
var actionFilterKeyword = ['search','find','login','reg'];   //无用表单黑名单,用于验证这个form表单有没有用(针对form表单验证)
}

现在的黑白名单列表:

var placeholderFilterKeyword = ['跳','搜','查','找','登陆','注册','search'];
var actionFilterKeyword = ['search','find','login','reg',"baidu.com","google.com","so.com","bing.com","soso.com","sogou.com"];

此处的代码,决定了整体插件检测时的误报率大体走向。你也可以自己修改来达到自我感觉不错的地步。

现在的初始化变量:

var actionCache,actionPath;
var actionvParameter = "";
var ajaxParameter = "";

0×02:检测token的机制优化

之前的token验证机制是针对于type属性为hidden的input标签里的value的值是否大于10。代码如下:

for(var j = 0;j < formDom.find(":hidden").length;j++){
    if(formDom.find(":hidden").eq(j).val().length > 10){
        continue outerFor;
    }
}

但是当我进一步测试的时候,发现这个误报率比较大。比如我在测试Freebuf主站的时候,FB的token值不到10位,但是他是toekn。那么就可以绕过之前的设定。当然了本章就已经解决了这个问题,使之在测试的时候,检测token的机制成功率达到95%以上。很少的情况下才会出现误报。OK,现在让我们来进行修复吧:

首先我先说明一下token机制的特性:

每刷新一次页面,token就会变化

我们可以针对此特性进行思考。优化后的token机制的思路:

使用JavaScript代码在页面里插入一个空白隐藏的iframe标签,再使用ajax请求,重新获取一下当前页面的源代码,至于为什么不使用document.documentElement.outerHTML来获取页面的源代码呢,很简单的原因,document.documentElement.outerHTML是不刷新获取,类似于我们按下Ctrl+U来查看源代码,而ajax发送后获取,类似于重新打开一次页面,再按下Ctrl+U。两者的不同就出在ajax是重新发送一次请求,就像刷新页面一样。

思路说完了,那我们该如何写代码呢,首先在第一行,outerFor:代码之上,写ajax获取源代码,因为outerFor:下面的代码都是在for循环了,我们不需要每循环一次就ajax获取一次。我只需要获取一次就行了。代码如下:

var iframe = document.createElement('iframe');
$("html").append("<iframe id ='tokenCheck' src='about:blank' style='display:none;'></iframe>");
$.ajax({
    url: location.href,
    type: 'get',
    dataType: 'html',
})
.done(function(data){
    $("#tokenCheck").contents().find("body").html(data);
})

这里的dataType必须是html。不然无法获取标签,然后使用$("#tokenCheck").contents().find("body").html(data);代码,把我们使用ajax获取到的源代码放到iframe标签里,如图:

然后我们回到检测token机制功能的代码处。修改一下代码为:

if(formDom.find(":hidden").length > 0){
    for(var j = 0;j < formDom.find(":hidden").length;j++){
        var tokenInputValue = formDom.find(":hidden").eq(j).val();
        if($($("#tokenCheck").contents()['context']['forms'][i]).find(":hidden").eq(j).val() != tokenInputValue){
            continue outerFor;
        }
    }
}

这里我新加入了if判断,当当前form表单里没有type属性为hidden的input标签时。则跳过此次的检测token机制的功能代码。for循环里,首先是赋值,把当前的input标签的里的value值赋值给tokenInputValue变量。

下面的代码就很重要了,获取iframe标签里相同的form标签及相同的input标签里的值。首先我们使用$("#tokenCheck").contents()来获取iframe标签里的内容。再使用['context']来获取html的DOM树,再使用['forms']来获取iframe里的form标签。然后使用最外层的i变量,使之iframe获得的form表单和我们正在处理的form时同一个。然后在最外层写上$(),使JavaScript对象变成jQuery变量,我们就可以使用jquery的API了。下面的find(":hidden").eq(j).val()就是获取相同form标签里的相同input标签里的值。再使用if判断,两个token的值是否一样:

if($($("#tokenCheck").contents()['context']['forms'][i]).find(":hidden").eq(j).val() != tokenInputValue){
    continue outerFor;
}

如果一样则不是token,如果不一样则是token。

0×03:插件的整体框架

因为Maxthon浏览器的API实在是太少,没有这些API我无法进行Referer检测,于是,检测CSRF插件,就不写Maxthon的插件了,下面是Chrome插件的框架:

icons 是存放插件图标的地方,我比较懒,直接使用AutoFindXSS插件的图标。

background.html 是为了让我们修改插件的作用域,让我们可控,可以在Chrome的API中使用jquery插件

background.js 这里我们把它理解为后端程序,类似于服务端的存在。用于处理base.js文件的数据

base.js 会在网站加载完成后调用。在检测Referer的时候,把数据传给background.js文件

manifest.json Chrome插件的核心文件,用于配置插件参数。

这里我先给大家看一下manifest.json文件的内容:

{
  "background": {
    "page": "background.html",
    "persistent": true
  },
  "name": "AutoFindCSRF",
  "version": "1.0.0",
  "manifest_version": 2,
  "description": "CSRF[by:Black-Hole&158099591@qq.com]", 
  "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
  "permissions": [     
    "<all_urls>","tabs"
  ],
  "icons":{"16": "icons/icon_16.png","48": "icons/icon_48.png","128": "icons/icon_128.png"},
  "content_scripts": [{
    "matches": ["*://*/*"],
    "js": ["jquery.js","base.js"],
    "run_at": "document_end"
  }]
}

content_security_policy 简称CSP,用户限制插件的安全性

permissions 是插件向Chrome申请的权限。

content_scripts 意思是,在任何协议下,当网站加载完成后,都会运行jquery.js和base.js文件。JavaScript this指向的是当前网页

background JavaScript this指向的是插件,用户处理base.js和background.js通信的存在

上一篇文章的JavaScript代码,都存放在base.js里,待会说“检测Referer机制”时,也是写在这个文件里。

0×04:检测对方是否开启了Referer检测机制

首先为了下面程序的简洁,先把当前表单的action地址赋值给一个变量:actionCache = formDom.attr("action");

然后匹配action地址。为什么要匹配action地址呢,因为action分为以下几种情况:

#test

./test.php && ./test(处理方式一样)

/test.php?a=11

test.php

http://baidu.com/?s=

这里我们使用switch来实现匹配,代码如下:

switch(actionCache[0]){
    case "#":
        actionPath = location.href + actionCache;
        break;
    case "/":
        actionPath = location.origin + actionCache;
        break;
    case ".":
        if(actionCache.indexOf("?") != "-1"){
            actionvParameter = "?" + actionCache.split("?")[1];
            actionCache = actionCache.slice(0,actionCache.indexOf("?"));
        }
        if(location.href.split("/").pop().split(".").length == 1){
            actionPath = location.href + actionCache.substr(1,actionCache.length-1) + actionvParameter;
        }else{
            actionPath = location.href.substr(location.href,location.href.lastIndexOf(location.href.split("/").pop())) + actionCache.substring(1,actionCache.length) + actionvParameter;
        }
        break;
    default:
        if(location.protocol == "http:" || location.protocol == "https:"){
            actionPath = location.href;
            break;
        }
        if(location.href.split("/").pop().split(".").length == 1){
            actionPath = location.href + "/" + actionCache;
        }else{
            actionPath = location.href.substr(location.href,location.href.lastIndexOf(location.href.split("/").pop())) + actionCache;
        }
        break;
}

当action地址的第一个值是#时,直接使用location.href + actionCache;拼接。

当action地址的第一个值是/时,使用location.origin + actionCache;来进行拼接

当action地址的第一个值是.时:先使用indexOf函数来把参数赋值给一个变量并去除,

if(actionCache.indexOf("?") != "-1"){
    actionvParameter = "?" + actionCache.split("?")[1];
    actionCache = actionCache.slice(0,actionCache.indexOf("?"));
}

详细的情况如下:

然后根据有无后缀进行匹配:

if(location.href.split("/").pop().split(".").length == 1){
    actionPath = location.href + actionCache.substr(1,actionCache.length-1) + actionvParameter;
}else{
    actionPath = location.href.substr(location.href,location.href.lastIndexOf(location.href.split("/").pop())) + actionCache.substring(1,actionCache.length) + actionvParameter;
}

location.href.split("/").pop().split(".").length是检测当前url有无后缀,如果有那么长度是为2.如果没有后缀长度是1。如果没有参数,将不会加任何字符串,因为在初始变量的时候就已经设为空了。详情如下:

除去这些之外,还有直接是文件名或者直接是url,这里呢,我直接写到switch的default分之上去了,因为无法使用actionCache[0]来匹配,代码如下:

default:
    if(location.protocol == "http:" || location.protocol == "https:"){
        actionPath = location.href;
        break;
    }
    if(location.href.split("/").pop().split(".").length == 1){
        actionPath = location.href + "/" + actionCache;
    }else{
        actionPath = location.href.substr(location.href,location.href.lastIndexOf(location.href.split("/").pop())) + actionCache;
    }
    break;

首先是判断location.protocol是否为http或https协议。如果是的话,直接使用location.href;。当不为http://或者https://的时候,跳过此if判断。接下来就是判断url的后缀存在。如果存在将运行:actionPath = location.href + "/" + actionCache;,反馈如图:

当存在后缀时,运行:actionPath = location.href.substr(location.href,location.href.lastIndexOf(location.href.split("/").pop())) + actionCache;。反馈如图:

0×05:模拟form的参数

代码如下:

for(var v = 0;v < formDom.find(":text").length;v++){
    var input = formDom.find(":text").eq(v);
    if(input.attr("name") != ""){
        if(input.val() == ""){
            ajaxParameter += input.attr("name") + "=" + "15874583485&";
        }else{
            ajaxParameter += input.attr("name") + "=" + input.val() + "&";
        }
    }else{
        continue;
    }
}
ajaxParameter = ajaxParameter.substring(0,ajaxParameter.length-1);

使用for循环对当前form表单下属性为text的input标签,然后使用var input = formDom.find(":text").eq(v);来进行赋值,把当前的input赋值给input变量。

再使用if判断,当前的input标签是否存在name属性,如果没有,则使用continue;跳出初始化表达式变量为v的本次循环。如果存在,再判断当前的input的value属性里是否有值,如果有值则直接赋值给ajaxParameter。代码:ajaxParameter += input.attr("name") + "=" + input.val() + "&";,如果不存在则把15874583485赋值给ajaxParameter变量,为什么要使用类似于手机号码的呢,因为容错率挺高的。可以看到我在每次赋值的时候,都会在后面加上&字符。因为方便下面发送ajax。当然需要去掉最后一个&。于是乎,有了下面的代码:ajaxParameter = ajaxParameter.substring(0,ajaxParameter.length-1);

0×04:与插件的background.js进行通信

这里呢,我先说说“检测Referer的思路”,在当前网站发送一次ajax请求,Referer的地址肯定是当前的URL,是正常的,和普通提交form表单是一样的,这里呢,把action地址和method值及参数传给插件,在插件里再发送一次AJAX请求,chrome插件发送AJAX时,Refere是为空的。两次提交,如果存在Referer检测,那么返回的结果长度肯定是不一样的,如果不存在Referer检测,长度是一样的(当然可能存在个别的差异,因为可能要显示时间等,结果长度不一样,但是是不存在“Referer检测”的,下面会增加容错率)

Chrome对插件通信提供了发送chrome.runtime.sendMessage和接受chrome.runtime.onMessage.addListener的API。首先让我们来看看base.js文件里的发送chrome.runtime.sendMessageAPI代码:

$.ajax({
    url: actionPath,
    type: (formDom.attr("method") == undefined) || (formDom.attr("method") == 'get')?'get':'post',
    dataType: 'html',
    data: (formDom.attr("method") == undefined) || (formDom.attr("method") == 'get')?'':ajaxParameter,
    async: false,
})
.done(function(data){
    var firstAjax = data.length;
    var formCache = formDom;
    chrome.runtime.sendMessage({action: actionPath, parameter: (formDom.attr("method") == undefined) || (formDom.attr("method") == 'get')?'':ajaxParameter},function (response) {
        if(Math.abs(firstAjax - response.status) < 10){
            formCache.attr("style","border: 1px red solid;")
        }
    });
})

因为form的method属性的值是不确定的。所以就需要对ajax的参数type进行设置:(formDom.attr("method") == undefined) || (formDom.attr("method") == 'get')?'get':'post',这里使用了三目运算符。当method的值不存在、为get的时候,type为get。当存在的时候,则为post。

下面的data参数同理。只不过没有了get、post选项。改为'':ajaxParameter。因为method值为get时,参数是附在actionPath变量里的。当为post的时候,将把之前拼接的参数传给data参数。这里计算一下返回页面的长度var firstAjax = data.length;,至于下面的为什么要给变量再赋值一次呢,我也不知道,可能下面的Chrome API的作用域不同,导致在下面使用API的时候,使用formDom变量,结果不对。只能重新赋值给formCache变量,这个时候API才算正常。

下面就是Chrome的API了:

chrome.runtime.sendMessage({action: actionPath, parameter: (formDom.attr("method") == undefined) || (formDom.attr("method") == 'get')?'':ajaxParameter},function (response) {
        if(Math.abs(firstAjax - response.status) < 10){
            formCache.attr("style","border: 1px red solid;")
        }
    });

这里的action和parameter是发送的参数及值。至于代码(formDom.attr("method") == undefined) || (formDom.attr("method") == 'get')?'':ajaxParameter和上面同理,当为get的时候,不给parameter值,当为post的时候,值为ajaxParameter。response为回调函数,类似ajax的done函数,返回background.js的处理结果。

那background.js是如何处理的呢:

chrome.runtime.onMessage.addListener(function(message,sender,sendResponse){
    $.ajax({
        url: message.action,
        type: (message.parameter == "")?'get':'post',
        dataType: 'html',
        data: (message.parameter == "")?'':message.parameter,
        async: false,
    })
    .done(function(data) {
        sendResponse({status: data.length})
    })
})

chrome.runtime.onMessage.addListener是接受函数,然后就是AJAX了,在done函数里,有一个API是sendResponse({status: data.length})返回插件发送AJAX时的长度。这个时候前端base.js将会受到background.js文件的返回结果。代码就返回上面的处理方式了:

if(Math.abs(firstAjax - response.status) < 10){
    formCache.attr("style","border: 1px red solid;")
}

这里的Math.abs是求绝对值的,当两次ajax返回的长度差值小于10的时候,说明不存在“Referer检测”,当大于10时,就说明存在“检测Referer的机制”了。这里的10就是容错值

当存在CSRF漏洞的时候,会在form表单的外部包含一个红色的框,如图:

整个代码如下:base.js

var iframe = document.createElement('iframe');
$("html").append("<iframe id ='tokenCheck' src='about:blank' style='display:none;'></iframe>");
$.ajax({
    url: location.href,
    type: 'get',
    dataType: 'html',
})
.done(function(data){
    $("#tokenCheck").contents().find("body").html(data);
})
outerFor:
for(var i = 0;i < $("form").length;i++){
    var formDom = $("form").eq(i);
    var imageFileSuffix = ['.jpg','.png','.jpge','.ico','.gif','.bmp'];
    var placeholderFilterKeyword = ['跳','搜','查','找','登陆','注册','search'];
    var actionFilterKeyword = ['search','find','login','reg',"baidu.com","google.com","so.com","bing.com","soso.com","sogou.com"];
    var actionCache,actionPath;
    var actionvParameter = "";
    var ajaxParameter = "";
    //去除类似搜索、页面跳转等无用的form表单
    if(formDom.attr("action") != undefined){
        var actionCheck = actionFilterKeyword.some(function(item,index){
            return (formDom.attr("action").toLowerCase().indexOf(item)  != "-1");
        })
        if(actionCheck){
            continue;
        }
    }else{
        continue;
    }
    for(var x = 0;x < formDom.find(":text").length;x++){
        var inputTextCheck;
        var inputText =  formDom.find(":text").eq(x);
        if(inputText.attr("placeholder") == undefined){
            continue;
        }
        inputTextCheck = placeholderFilterKeyword.some(function(item,index){
            return (inputText.attr("placeholder").toLowerCase().indexOf(item)  != "-1");
        })
        if(inputTextCheck){
            continue outerFor;
        }
    }
    //去除没有提交按钮的form表单
    if(formDom.find(":submit").length < 1){
        continue outerFor;
    }
    //去除具有token的form表单
    if(formDom.find(":hidden").length > 0){
        for(var j = 0;j < formDom.find(":hidden").length;j++){
            var tokenInputValue = formDom.find(":hidden").eq(j).val();
            console.log($($("#tokenCheck").contents()['context']['forms'][i]).find(":hidden").eq(j).val(),tokenInputValue)
            if($("#tokenCheck").contents().find("form").eq(i).find(":hidden").eq(j).val() != tokenInputValue){
                continue outerFor;
            }
        }
    }
    //去除带有验证码的form表单
    if(formDom.find("img").length > 0){
        var imageCheck;
        for(var z = 0;z < formDom.find("img").length;z++){
            var img = formDom.find("img").eq(z);
            var imgSrc = img.attr("src")
            if(!!imgSrc){
                if(imgSrc.indexOf("?") != "-1"){
                    imgSrc = imgSrc.slice(0,imgSrc.indexOf("?"));
                }
                imgSrc = imgSrc.substr(imgSrc.lastIndexOf("."),imgSrc.length);
                imageCheck = imageFileSuffix.some(function(item,index){
                    return (imgSrc == item);
                })
                if(!imageCheck){
                    continue outerFor;
                }
            }
        }
    }
    //去除“检测对方开启了Referer检测机制”的form表单
    actionCache = formDom.attr("action");
    switch(actionCache[0]){
        case "#":
            actionPath = location.href + actionCache;
            break;
        case "/":
            actionPath = location.origin + actionCache;
            break;
        case ".":
            if(actionCache.indexOf("?") != "-1"){
                actionvParameter = "?" + actionCache.split("?")[1];
                actionCache = actionCache.slice(0,actionCache.indexOf("?"));
            }
            if(location.href.split("/").pop().split(".").length == 1){
                actionPath = location.href + actionCache.substr(1,actionCache.length-1) + actionvParameter;
            }else{
                actionPath = location.href.substr(location.href,location.href.lastIndexOf(location.href.split("/").pop())) + actionCache.substring(1,actionCache.length) + actionvParameter;
            }
            break;
        default:
            if(location.protocol == "http:" || location.protocol == "https:"){
                actionPath = location.href;
                break;
            }
            if(location.href.split("/").pop().split(".").length == 1){
                actionPath = location.href + "/" + actionCache;
            }else{
                actionPath = location.href.substr(location.href,location.href.lastIndexOf(location.href.split("/").pop())) + actionCache;
            }
            break;
    }
    for(var v = 0;v < formDom.find(":text").length;v++){
        var input = formDom.find(":text").eq(v);
        if(input.attr("name") != ""){
            if(input.val() == ""){
                ajaxParameter += input.attr("name") + "=" + "15874583485&";
            }else{
                ajaxParameter += input.attr("name") + "=" + input.val() + "&";
            }
        }else{
            continue;
        }
    }
    ajaxParameter = ajaxParameter.substring(0,ajaxParameter.length-1)
    $.ajax({
        url: actionPath,
        type: (formDom.attr("method") == undefined)?'get':'post',
        dataType: 'html',
        data: (formDom.attr("method") == undefined) || (formDom.attr("method") == 'get')?'':ajaxParameter,
        async: false,
    })
    .done(function(data){
        var firstAjax = data.length;
        var formCache = formDom;
        chrome.runtime.sendMessage({action: actionPath, parameter: (formDom.attr("method") == undefined) || (formDom.attr("method") == 'get')?'':ajaxParameter},function (response) {
            if(Math.abs(firstAjax - response.status) < 10){
                formCache.attr("style","border: 1px red solid;")
            }
        });
    })
}

background.js:

chrome.runtime.onMessage.addListener(function(message,sender,sendResponse){
    $.ajax({
        url: message.action,
        type: (message.parameter == "")?'get':'post',
        dataType: 'html',
        data: (message.parameter == "")?'':message.parameter,
        async: false,
    })
    .done(function(data) {
        sendResponse({status: data.length})
    })
})
此条目发表在未分类分类目录。将固定链接加入收藏夹。