自动化检测CSRF(第二篇)

2016-06-29 455061人围观 ,发现 6 个不明物体 WEB安全特别企划

前言

上一篇只是大致说明整个思路和流程。本篇就详细说说如何检测CSRF。为什么不在上一篇中放出插件呢。是因为误报率确实是比较多,而且无法检测Referer。而本章,重点就说明“如何检测对方是否开启了Referer检测机制”。

在我的认知范围内,这是首款检测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})
    })
})

0×06:结尾

文章非常感谢:北风(2660668090)提供的思路及技术支持。

插件下载地址:http://pan.baidu.com/s/1geMUl7l,顺便给自己的blog打个广告:http://www.bugs.cc/

奖励一下作者,让作者攒钱买媳妇:

*本文作者:Black-Hole,本文属FreeBuf原创奖励计划,未经许可禁止转载

相关推荐

这些评论亮了

发表评论

已有 6 条评论

取消
Loading...

特别推荐

填写个人信息

姓名
电话
邮箱
公司
行业
职位
css.php