22

从零起步做一个拦截蜜罐 XSSI 的 Chrome 扩展

 3 years ago
source link: https://mp.weixin.qq.com/s/biYBmjFdQEBc9owBqOsJUA
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

3E7Vrir.gif!mobile

一、起因

随着今年某大型实战攻防演习活动的结束,不仅攻击侧展现了各式各样的武器弹药,防守侧的防护方案和策略也让人拓展了思路。

RFza2qB.jpg!mobile

万能防守技

由于防守侧对攻击行为的发现及溯源是加分的,所以蜜罐基本上成为了今年防守的必选项,我们在今年多次实战演习和红队评估项目中均有发现蜜罐的应用。

为了实现对攻击者进行溯源和追踪,发现的WEB蜜罐中普遍使用了XSSI和Fingerprint相关技术。利用国内互联网大厂普遍通过JSONP跨域传输用户信息(没错,就是国内,国外大厂基本不用JSONP)的漏洞来实现对攻击者的身份截获。利用浏览器在使用代理或进入隐身模式后的某些特征不变性(canvas、font、webGL、audio等对象中的固定特征)来标识浏览器,再结合一些类似evercookie的方法来持久化身份标识,实现行为跟踪。

对于这些溯源技术,直接安装NoScript、ScriptSafe等浏览器扩展是可以达到一劳永逸的效果的。但这些扩展的误杀实在是太严重了,非常影响正常的网络访问体验。同时也希望能够在一定程度上发现蜜罐并予以提示,因此,萌生了开发一个Chrome扩展的想法。

3E7Vrir.gif!mobile

二、扩展的基本功能和实现思路

2.1 基本功能

通过分析几个蜜罐的功能和特性,希望扩展能够具备如下几个功能:

1) 截获页面中发起的XSSI请求,通过特征识别阻断可疑的XSSI

2) 分析和攫取蜜罐固有特征,识别蜜罐并拦截所有请求

3) 判断fingerprintjs库是否存在并提示,判断是否有其他web指纹的相关调用

4) 判断是否有持久化身份标识的相关调用

2.2 实现思路

如果第一次开发浏览器扩展,小茗同学的 Chrome插件(扩展)开发全攻略 是一份非常不错的参考文档。再参考Chrome扩展的官方开发文档,只要具备常规的WEB前端开发技能(HTML、CSS、JavaScript)便可以开发出一个不错的扩展来。

Chrome扩展的一切开始于manifest.json,可在该文件配置不同类型脚本,使之具备不同的生命周期和功能:

1) background脚本的生命周期伴随着整个浏览器,不区分tab,可以通过添加webRequest监听器来实现对请求的拦截和阻断。

2) content_scripts脚本的生命伴随着每一个tab页面,可以实现对DOM的查看和操作,但不能实现对页面中JS的调用。可以用来做“基本功能”中提到的2-4的相关功能点。

如下是一个简要的manifest.json配置项:

{
"manifest_version": 2,
"name": "AntiHoneypot",
"version": "0.0.5",
"background": {
"page": "background.html"
},
"content_scripts": [
{
"js": ["content-script.js"],
}
],
"permissions": [
"webRequest",
"webRequestBlocking",
"notifications"
],
"homepage_url": "https://www.monyer.com"
}

需要注意的是:由于content_scripts没办法调用页面中的JS,所以我们需要DOM操作将预执行的JS注入到页面中,方法跟正常的JS注入也类似。

var script_1 = document.createElement('script');
script_1.textContent = "(" + inject + ")()";
document.documentElement.appendChild(script_1);

另外,由于各自生命周期不同,互相间传递信息需要通过消息来通信。页面中的JS只能与content-scripts传递信息,可通过window.postMessage和window.addEventListener实现。content-script向background传递消息需要通过chrome.runtime.sendMessage和chrome.runtime.onMessage.addListener实现。popup从background获取信息较为方便,通过chrome.extension.getBackgroundPage()来获取background对象,然后便可以直接调用background中的变量或方法了。具体的实现方式参见本扩展的代码或者小茗同学的文档,此处不再展开。

有了上述的基础垫底,我们可以梳理出功能的实现思路:

1) 在background中添加webRequest监听器,通过特征识别阻断可疑的XSSI。

2) 在content-scripts中通过DOM的判断,或者通过注入JS对页面中JS变量或方法进行判断来识别fingerprint、指纹调用和身份标识持久化等操作。通过消息传递,将信息传递到background或popup,实现提示功能。

3E7Vrir.gif!mobile

三、功能实现

3.1 XSSI识别和阻断实现

chrome扩展对网络请求的阻断是通过webRequest实现的。通过查看webRequest API文档,我们最终选择onBeforeSendHeaders和onHeadersReceived两个事件API来截获请求。这两个事件API一个是在HTTP已请求建立,但是Header还未发送前触发;一个是在服务器端返回的HTTP Header已到达本地,但body还没到达前触发。这两个API都具备阻断请求的功能。

U3yaA3J.png!mobile

chrome extension webrequest api

虽然对比onBeforeSendHeaders,onBeforeRequest更为靠前,此时TCP链接尚未建立。但onBeforeRequest阶段没办法获得和修改HTTP请求中的Header,譬如Cookie等。所以综合考虑,使用onBeforeSendHeaders更为合适。

我们通过调用API的addListener方法来实现对请求的捕获,url过滤规则设置成所有,并添加阻断、额外头、发送头、响应头等权限。之后当请求发起时,在对应的事件节点,便会调用我们设置的回调函数。

//设置监听器于Header发送开始前
chrome.webRequest.onBeforeSendHeaders.addListener(
beforeSendHeaders, {
urls: ["<all_urls>"]
}, ['blocking', 'extraHeaders', 'requestHeaders']
);
//设置监听器于服务器端header发送后,body发送之前
chrome.webRequest.onHeadersReceived.addListener(
headersReceived, {
urls: ['<all_urls>']
}, ['blocking', 'extraHeaders', 'responseHeaders']
);

通过控制回调函数的返回值,我们便可以实现对本次请求的阻断。

//当回调函数返回如下值时,请求将被阻断。
return {
cancel: true
};

onBeforeSendHeaders和onHeadersReceived的回调函数参数均为details,是一个包含请求类型、请求url、请求类型、发起者url、tabId等信息的对象。

chrome默认的请求类型有:"main_frame", "sub_frame", "stylesheet", "script", "image", "font", "object", "xmlhttprequest", "ping", "csp_report", "media", "websocket", "other"。XSSI的利用基本上都是“script”类型。在蜜罐中,我们也发现通过创建隐藏iframe,利用XSS获取虚拟身份的办法,所以"sub_frame"的类型也在监控之列。在很久之前,有人琢磨出通过远程调用css的方式来跨域获取一定敏感数据的方法,不过利用条件挺苛刻,所以"stylesheet"类型我们暂不考虑。

由于只针对跨域情况,所以首先通过判断请求url和发起者url,将同顶级域名请求放行,之后便可以根据请求链接中域名的特征、URI的特征、QueryString的特征对请求进行判断和拦截。

//利用自定义函数获取发起者和请求者URL的域名和顶级域名
let {
topDomain: initiatorTopDomain
} = _getDomain(details.initiator);
let {
topDomain: urlTopDomain
} = _getDomain(details.url);
//域名相同不拦截,顶级域名相同不拦截。
if (initiatorTopDomain == urlTopDomain) {
return;
}
if (['script', 'xmlhttprequest', 'sub_frame'].includes(details.type)) {
//ban掉蜜罐中出现过的jsonp域名
let blackJsonpDomain = ['comment.api.163.com', 'now.qq.com', 'node.video.qq.com', 'passport.game.renren.com', 'wap.sogou.com', 'v2.sohu.com', 'login.sina.com.cn', 'bbs.zhibo8.cc', 'appscan.360.cn', 'wz.cnblogs.com', 'api.csdn.net', 'so.v.ifeng.com', 'api-live.iqiyi.com', 'account.itpub.net', 'm.mi.com', 'hudong.vip.youku.com', 'home.51cto.com', 'passport.baidu.com', 'chinaunix.net', 'www.cndns.com', 'remind.hupu.com', 'api.m.jd.com', 'passport.tianya.cn', 'my.zol.com.cn', 'account.cnblogs.com', 'pcw-api.iqiyi.com', 'stadig.ifeng.com', 'account.xiaomi.com', 'cmstool.youku.com', 'api.ip.sb', 'log.mmstat.com', 's1.mi.com', 'g.alicdn.com', 'fourier.taobao.com', 'cndns.com', 'sitestar.cn', 'tie.163.com', 'musicapi.taihe.com'];
if (blockKeywords(urlDomain, blackJsonpDomain, "black jsonp domain", details)) {
return {
cancel: true
};
}
//ban掉其他危险的域名,譬如统计网站。其实对于防追踪,用一些adblock扩展会更全一些,效果更好
let blackOtherDomain = ['hm.baidu.com', 'cnzz.com', '51.la', 'google-analytics.com', 'googletagservices.com'];
if (blockKeywords(urlDomain, blackOtherDomain, "danger domain", details)) {
return {
cancel: true
};
}
//ban掉uri部分中含有.json、jsonp等关键词的请求
let blackUriKeywords = ['.json', 'jsonp'];
if (blockKeywords(url.split("?").slice(0, 1).join('?'), blackUriKeywords, "black url keyword", details)) {
return {
cancel: true
};
}
//ban掉querystring部分中含有callback等关键词的请求
let blackQueryKeyWords = ["callback", "jsonp", "token=", "=json", "json=", "=jquery", "js_token", "window.name", "eval("];
if (blockKeywords(url.split("?").slice(1).join('?'), blackQueryKeyWords, "black query keyword", details)) {
return {
cancel: true
};
}
}

如果想再严格一些,我们可以再判断下返回header的头,看是否存在“x-powered-by”,或者content-type是否为“json”或“text/html”。一般这几种情况下,js为动态生成的概率很大。当然这种策略的误杀率也会蛮高,很可能会导致页面功能失常。如果想在误杀和宁缺毋滥之间找个平衡的话,可以使用另外一种策略:不对打中的请求进行阻断,但对请求头中的Cookie字段进行移除。这样请求依然会发送和接收,但因为少了Cookie,自然也就不再会获得用户隐私信息。

/**
* 移除requestHeaders中的Cookie字段
* @param {*} details
*/
function _removeCookie(details) {
for (var i = 0; i < details.requestHeaders.length; ++i) {
if (details.requestHeaders[i].name.toLowerCase() === 'cookie') {
details.requestHeaders.splice(i, 1);
break;
}
}
return {
requestHeaders: details.requestHeaders
};
}

3.2 蜜罐的识别

一个完美的蜜罐本应该除了IP、域名和数据外,所有的一切都跟正常的应用系统一模一样,是不可能被识别出来的。如果一个蜜罐出现了可被识别的特征,那么必然是因为画蛇添足加了不该加的东西,譬如上面提到的XSSI的利用。通过对蜜罐进行分析,还发现有FingerprintJS和evercookie的利用,这些都可以作为识别的特征。

某蜜罐除了这些外还有两个特征:一个是自定义了header中的server字段,另一个是页面中定义了两个JS变量。

针对自定义的server字段,在onHeadersReceived阶段判断所有请求返回的header即可。

let getHeader = headerKey => details.responseHeaders.filter(header => header.name.toLowerCase() == headerKey);
//某蜜罐服务器的Server字段特征
let headerServerBlackKeywords = ['*****'];
let headerServer = getHeader('server');
if (headerServer.length !== 0 &&
blockKeywords(headerServer[0].value, headerServerBlackKeywords, "black header[server]", details)
) {
return cancel;
}

针对页面中定义的变量判断起来略微麻烦一些,大致流程如下:

1) content-script进行DOM操作,向页面插入脚本

·页面脚本执行,并生成结果

·页面脚本调用window.postMessage发送消息

2) content-script通过window.addEventListener监听消息

3) conent-script调用chrome.runtime.sendMessage发送消息

·background通过chrome.runtime.onMessage.addListener接收消息

·background执行后续操作流程,譬如全局控制或阻断

var inject = function() {
//某蜜罐用了两个全局变量:token、path。token为使用短横线链接的随机值,path为js_开头的目录
if (window.token !== undefined && window.path !== undefined) {
if (typeof token == "string" && token.includes("-") &&
typeof path == "string" && path.includes("js_")) {
document.documentElement.innerHTML = "This is a Honeypot.";
window.postMessage({
honeypot: true,
blockInfo: "token=" + token + " | path=" + path
}, '*');
}
}
};


var script_1 = document.createElement('script');
script_1.textContent = "(" + inject + ")()";
document.documentElement.appendChild(script_1);


window.addEventListener("message", function(e) {
if (e.data && e.data.honeypot !== undefined) {
console.log("这是一个蜜罐");
chrome.runtime.sendMessage({
honeypot: true,
blockInfo: e.data.blockInfo
});
}
}, false);

注:对前端页面的判断,在不知本扩展判断规则的前提下,并不好规避。不过在公开判断规则的情况下,想要规避就轻而易举了。

3.3 FingerprintJS识别和指纹调用监控实现

FingerprintJS的判断采取了与上面蜜罐判断类似的办法,主要是判断window下的全局函数是否有x64hash128和getV18两个方法,这是它所独有的。

//如果使用了fingerprint库,全局函数的内部存在x64hash128和getV18两个对外公开的函数,以此作为判断。
var fp = Object.keys(window).filter(func => func != 'webkitStorageInfo' && typeof window[func] == "function" && window[func]['x64hash128'] && window[func]['getV18']);
if (fp.length !== 0) {
window.postMessage({
fingerprint2: true,
fp: fp.join(',')
}, '*');
}

指纹调用识别采取的拿来主义,把mybrowseraddon.com的AudioContext Fingerprint Defender、Canvas Fingerprint Defender、Font Fingerprint Defender、WebGL Fingerprint Defender四个扩展的脚本直接拷过来了。这样四个扩展变一个,可同时实现对四种指纹的混淆,同时也可直接对四种指纹的提取进行监控。当一个页面同时调用了四种指纹提取所需要的函数时,页面上可以弹出一个通知。(也正是因为Copy了四份代码,所以本扩展代码仅作为Chrome扩展编写学习和个人研究使用,并不会发布到chrome网上应用店中)

3.4 识别身份持久化标识的实现

通过测试evercookie、FingerprintJS Pro Demo并查阅相关资料发现,目前通用性较好的做身份标识持久化的方法大体上有两大种:一种主要利用浏览器的缓存来实现,譬如利用特殊的etag值、利用cache缓存在本地的资源等;另一种是利用浏览器针对本站的存储空间,包括:Cookies、LocalStorage、SessionStorage、indexedDB、WebSQL等。

aqIfA3Z.jpg!mobile

对于浏览器缓存,没有太好的办法,要么禁止本地缓存,譬如打开并勾选DevTools下Network的Disable cache,或者是安装Classic Cache Killer等扩展,再或者每隔一段时间做一次清理。

对于浏览器存储空间,针对Cookies、LocalStorage、SessionStorage三种存储,由于都是key-value模式,可以直接利用JS获取其中的值,然后两两比较。当其中某个存储空间中的值与另外一个存储空间的相等时,即说明网页开发者有意想多处保留数据,那么是一个身份持久化标识的可能性就很大,就可以让扩展弹出一个警告通知。

//试验性质的功能,check evercookie。比较localStorage、sessionStorage、cookies
if (request.ls !== undefined && request.ss !== undefined) {
chrome.cookies.getAll({
url: sender.url
}, cos => {
var co_vals = [];
cos.forEach(c => co_vals.push(c.value));
ls_vals = Object.values(request.ls);
ss_vals = Object.values(request.ss);


sameIdCompare(ls_vals, ss_vals, urlDomain);
sameIdCompare(co_vals, ls_vals, urlDomain);
sameIdCompare(ss_vals, co_vals, urlDomain);
// console.log(sameIds);
});
}

对于indexedDB、WebSQL的情况,因为用的人很少,其次里面又是数据库、又是表、又是字段,结构复杂。所以并没有采取枚举每个字段值去比较(WebSQL是一个废弃的功能,甚至连枚举数据库的功能都没有),而是直接劫持了indexedDB.open和openDatabase两个函数。当这两个函数被调用时,触发警告通知。然后需要大家慧眼识珠,人肉去鉴别这个行为的“正义”和“邪恶”了……

var inject = function() {
//劫持openDataBase
const openDb = openDatabase;
openDatabase = (...args) => {
window.top.postMessage("openDatabase-alert", '*');
return openDb(...args);
}
//劫持indexedDB.open
const idxDbOpen = indexedDB.__proto__.open;
Object.defineProperty(indexedDB.__proto__, "open", {
"value": function() {
window.top.postMessage("indexedDB-alert", '*');
return idxDbOpen.apply(this, arguments);
}
});
};
var script_1 = document.createElement('script');
script_1.textContent = "(" + inject + ")()";
document.documentElement.appendChild(script_1);

到此为止,我们对于该扩展所有预想的功能便都实现了。

剩下的就是代码组装调试、icon状态控制、popup.html扩展弹出页、options.html选项页等常规的杂七杂八的功能实现了,本文就不多赘述了。

3E7Vrir.gif!mobile

四、 ANTI-CSRF和ANTI-XSSI大杀器SameSite

今年2020年2月4日谷歌发布了Chrome的80版本,其中有一个很大的变化就是将Cookie中SameSite属性的默认值从None变成了Lax。虽然是仅仅是改变了Cookie一个属性的默认值这么小的变化,但是基本上一下子就把CSRF和XSSI给干掉了。

SameSite属性是谷歌于2016年4月向IETF提交的一个用于改善跨域请求携带Cookies的解决方案标准。Chrome随即从51版本,开始支持这项功能。

SameSite可以设置三个值:Strict、Lax、None。

设置cookie时的用法如下:

Set-Cookie: CookieName=CookieValue; SameSite=Strict;

当SameSite为Strict时,出现任何跨站点请求时,均不会带上这个Cookie。这里说的“任何”包括页面中的所有请求类型,甚至是从某一个网站点击本网站的链接,都不会带上这个Cookie。这种点击链接跳转都不带Cookie很明显在通常情况下是很难承受的,所以在大部分情况下,我们都应该选择Lax模式。

当设置为Lax时,为宽松模式。允许三种情况下的跨站访问携带本Cookie,分别是:弹出新窗口、顶级页面跳转和预加载模式(prerendering)。其他不管是script、iframe、css还是img的跨站点加载均不会发送Cookie。这使得CSRF和XSSI基本上被终结了。

当设置为None时,跟往常一样。可以任意地跨站点传递该Cookie。但SameSite=None与Cookie的另外一个属性“Secure”形成了互斥关系,即如果设置SameSite=None,则必须同时设置Secure属性,否则无效。意味着如果要跨域传递Cookie则必须使用HTTPS模式,HTTP下无论如何都传递不了。

不过即便SameSite设置如此严格,但由于各大网站的开发工程师为了兼容性在疯狂地给SameSite设置None值,所以未来这种情况下的XSSI依然会存在。再加上上述实现的其他几项功能,本扩展依然有着它的的实用性和价值。

代码下载&扩展使用:

1) 下载代码 https://github.com/Monyer/antiHoneypot

2) 解压到本地合适位置

3) 打开chrome扩展程序页

4) 切换到开发者模式

5) 使用“加载已解压的扩展程序”加载扩展即可

3E7Vrir.gif!mobile

五、 参考文献

https://scotthelme.co.uk/csrf-is-dead/

https://tools.ietf.org/html/draft-west-first-party-cookies-07

http://www.ruanyifeng.com/blog/2019/09/cookie-samesite.html

http://blog.haoji.me/chrome-plugin-develop.html

https://developer.chrome.com/extensions/

https://mybrowseraddon.com/audiocontext-defender.html

https://samy.pl/evercookie/

https://fingerprintjs.com/demo

本文作者:奇安信高级攻防部 Monyer。目前奇安信安服红队发出江湖召集令,凡是具备实战渗透经验、红队经验、APT实战能力的大侠,均可前来一试,全国招募!联系邮箱:[email protected]

Z73In2j.png!mobile

RVzUJza.gif!mobile

让网络更安全

让世界更美好

长按识别二维码关注我们

NRz6Vb.jpg!mobile


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK