Browser Extension Pentesting Methodology

支持 HackTricks

基本信息

浏览器扩展是用 JavaScript 编写的,并在后台由浏览器加载。它有自己的 DOM,但可以与其他网站的 DOM 交互。这意味着它可能会危害其他网站的机密性、完整性和可用性 (CIA)。

主要组件

扩展布局在可视化时效果最佳,包含三个组件。让我们深入了解每个组件。

内容脚本

每个内容脚本可以直接访问 单个网页 的 DOM,因此暴露于 潜在恶意输入。然而,内容脚本除了能够向扩展核心发送消息外,没有其他权限。

扩展核心

扩展核心包含大部分扩展权限/访问,但扩展核心只能通过 XMLHttpRequest 和内容脚本与网页内容交互。此外,扩展核心无法直接访问主机机器。

本地二进制

扩展允许一个本地二进制文件,可以 以用户的完全权限访问主机机器。 本地二进制通过 Flash 和其他浏览器插件使用的标准 Netscape 插件应用程序编程接口 (NPAPI) 与扩展核心交互。

边界

为了获得用户的完全权限,攻击者必须说服扩展将恶意输入从内容脚本传递到扩展核心,并从扩展核心传递到本地二进制。

扩展的每个组件之间由 强保护边界 隔离。每个组件在 单独的操作系统进程 中运行。内容脚本和扩展核心在 沙箱进程 中运行,这些进程对大多数操作系统服务不可用。

此外,内容脚本通过 在单独的 JavaScript 堆中运行 与其关联的网页分离。内容脚本和网页可以 访问相同的底层 DOM,但两者 从不交换 JavaScript 指针,防止 JavaScript 功能的泄露。

manifest.json

Chrome 扩展只是一个带有 .crx 文件扩展名 的 ZIP 文件夹。扩展的核心是位于文件夹根目录的 manifest.json 文件,该文件指定布局、权限和其他配置选项。

示例:

{
"manifest_version": 2,
"name": "My extension",
"version": "1.0",
"permissions": [
"storage"
],
"content_scripts": [
{
"js": [
"script.js"
],
"matches": [
"https://example.com/*",
"https://www.example.com/*"
],
"exclude_matches": ["*://*/*business*"],
}
],
"background": {
"scripts": [
"background.js"
]
},
"options_ui": {
"page": "options.html"
}
}

content_scripts

内容脚本在用户导航到匹配页面加载,在我们的例子中,任何匹配**https://example.com/*表达式且不匹配*://*/*/business*正则表达式的页面。它们像页面自己的脚本**一样执行,并且可以任意访问页面的文档对象模型 (DOM)

"content_scripts": [
{
"js": [
"script.js"
],
"matches": [
"https://example.com/*",
"https://www.example.com/*"
],
"exclude_matches": ["*://*/*business*"],
}
],

为了包含或排除更多的 URL,您还可以使用 include_globsexclude_globs

这是一个示例内容脚本,当使用 存储 API 从扩展的存储中检索 message 值时,将向页面添加一个解释按钮。

chrome.storage.local.get("message", result =>
{
let div = document.createElement("div");
div.innerHTML = result.message + " <button>Explain</button>";
div.querySelector("button").addEventListener("click", () =>
{
chrome.runtime.sendMessage("explain");
});
document.body.appendChild(div);
});

当点击此按钮时,通过利用runtime.sendMessage() API,内容脚本向扩展页面发送消息。这是由于内容脚本在直接访问API方面的限制,storage是少数例外之一。对于超出这些例外的功能,消息被发送到扩展页面,内容脚本可以与之通信。

根据浏览器的不同,内容脚本的能力可能会略有不同。对于基于Chromium的浏览器,能力列表可在Chrome Developers documentation中找到,而对于Firefox,MDN是主要来源。 值得注意的是,内容脚本能够与后台脚本进行通信,使其能够执行操作并传递响应。

要在Chrome中查看和调试内容脚本,可以通过选项 > 更多工具 > 开发者工具访问Chrome开发者工具菜单,或按Ctrl + Shift + I。

当开发者工具显示后,点击选项卡,然后点击内容脚本选项卡。这允许观察来自各种扩展的运行内容脚本,并设置断点以跟踪执行流程。

注入的内容脚本

请注意,内容脚本不是强制性的,也可以通过**tabs.executeScript** 动态 注入脚本并在网页中以编程方式注入。这实际上提供了更细粒度的控制

对于内容脚本的程序化注入,扩展需要对要注入脚本的页面具有主机权限。这些权限可以通过在扩展的清单中请求它们或通过activeTab临时获得。

示例基于activeTab的扩展

manifest.json
{
"name": "My extension",
...
"permissions": [
"activeTab",
"scripting"
],
"background": {
"service_worker": "background.js"
},
"action": {
"default_title": "Action Button"
}
}
  • 点击时注入一个JS文件:

// content-script.js
document.body.style.backgroundColor = "orange";

//service-worker.js - Inject the JS file
chrome.action.onClicked.addListener((tab) => {
chrome.scripting.executeScript({
target: { tabId: tab.id },
files: ["content-script.js"]
});
});
  • 在点击时注入一个函数:

//service-worker.js - Inject a function
function injectedFunction() {
document.body.style.backgroundColor = "orange";
}

chrome.action.onClicked.addListener((tab) => {
chrome.scripting.executeScript({
target : {tabId : tab.id},
func : injectedFunction,
});
});

示例:带脚本权限

// service-workser.js
chrome.scripting.registerContentScripts([{
id : "test",
matches : [ "https://*.example.com/*" ],
excludeMatches : [ "*://*/*business*" ],
js : [ "contentScript.js" ],
}]);

// Another example
chrome.tabs.executeScript(tabId, { file: "content_script.js" });

为了包含或排除更多的 URL,您还可以使用 include_globsexclude_globs

内容脚本 run_at

run_at 字段控制 JavaScript 文件何时注入到网页中。首选和默认值是 "document_idle"

可能的值有:

  • document_idle:尽可能地

  • document_start:在任何来自 css 的文件之后,但在构建任何其他 DOM 或运行任何其他脚本之前。

  • document_end:在 DOM 完成后立即,但在图像和框架等子资源加载之前。

通过 manifest.json

{
"name": "My extension",
...
"content_scripts": [
{
"matches": ["https://*.example.com/*"],
"run_at": "document_idle",
"js": ["contentScript.js"]
}
],
...
}

通过 service-worker.js

chrome.scripting.registerContentScripts([{
id : "test",
matches : [ "https://*.example.com/*" ],
runAt : "document_idle",
js : [ "contentScript.js" ],
}]);

background

由内容脚本发送的消息由背景页面接收,背景页面在协调扩展组件中发挥着核心作用。值得注意的是,背景页面在扩展的整个生命周期中持续存在,默默运行而无需直接用户交互。它拥有自己的文档对象模型(DOM),能够实现复杂的交互和状态管理。

关键点

  • 背景页面角色: 作为扩展的神经中枢,确保扩展各部分之间的通信和协调。

  • 持久性: 它是一个始终存在的实体,对用户不可见,但对扩展的功能至关重要。

  • 自动生成: 如果未明确定义,浏览器将自动创建一个背景页面。这个自动生成的页面将包含扩展清单中指定的所有背景脚本,确保扩展的后台任务无缝运行。

浏览器在自动生成背景页面(当未明确声明时)所提供的便利,确保所有必要的背景脚本都被集成并正常运行,从而简化了扩展的设置过程。

示例背景脚本:

chrome.runtime.onMessage.addListener((request, sender, sendResponse) =>
{
if (request == "explain")
{
chrome.tabs.create({ url: "https://example.net/explanation" });
}
})

它使用 runtime.onMessage API 来监听消息。当接收到 "explain" 消息时,它使用 tabs API 在新标签页中打开一个页面。

要调试后台脚本,您可以转到 扩展详细信息并检查服务工作者, 这将打开带有后台脚本的开发者工具:

选项页面和其他

浏览器扩展可以包含各种类型的页面:

  • 操作页面 在点击扩展图标时显示在 下拉菜单中

  • 扩展将 在新标签页中加载的页面

  • 选项页面:此页面在点击时显示在扩展顶部。在之前的清单中,我能够通过 chrome://extensions/?options=fadlhnelkbeojnebcbkacjilhnbjfjca 访问此页面或点击:

请注意,这些页面不像后台页面那样持久,因为它们根据需要动态加载内容。尽管如此,它们与后台页面共享某些功能:

  • 与内容脚本的通信: 类似于后台页面,这些页面可以接收来自内容脚本的消息,促进扩展内的交互。

  • 访问扩展特定的 API: 这些页面享有对扩展特定 API 的全面访问,受扩展定义的权限限制。

permissions & host_permissions

permissionshost_permissionsmanifest.json 中的条目,指示 浏览器扩展具有哪些权限(存储、位置等)以及 在哪些网页上

由于浏览器扩展可能具有如此 特权,恶意扩展或被攻击的扩展可能允许攻击者 以不同方式窃取敏感信息并监视用户

检查这些设置如何工作以及它们如何被滥用:

content_security_policy

内容安全策略 也可以在 manifest.json 中声明。如果定义了内容安全策略,它可能是 脆弱的

浏览器扩展页面的默认设置相当严格:

script-src 'self'; object-src 'self';

有关CSP和潜在绕过的更多信息,请查看:

web_accessible_resources

为了让网页访问浏览器扩展的页面,例如一个.html页面,该页面需要在manifest.json的**web_accessible_resources**字段中提及。 例如:

{
...
"web_accessible_resources": [
{
"resources": [ "images/*.png" ],
"matches": [ "https://example.com/*" ]
},
{
"resources": [ "fonts/*.woff" ],
"matches": [ "https://example.com/*" ]
}
],
...
}

这些页面可以通过以下URL访问:

chrome-extension://<extension-id>/message.html

在公共扩展中,extension-id 是可访问的

不过,如果使用 manifest.json 参数 use_dynamic_url,则该 id 可能是动态的

请注意,即使此处提到某个页面,它也可能由于 内容安全策略受到 ClickJacking 保护。因此,在确认 ClickJacking 攻击是否可能之前,您还需要检查它(frame-ancestors 部分)。

允许访问这些页面使这些页面 可能容易受到 ClickJacking 攻击

仅允许这些页面由扩展加载,而不是由随机 URL 加载,可以防止 ClickJacking 攻击。

请注意,web_accessible_resources 中的页面和扩展的其他页面也能够 联系后台脚本。因此,如果这些页面中的一个容易受到 XSS 攻击,它可能会打开更大的漏洞。

此外,请注意,您只能在 iframe 中打开 web_accessible_resources 中指示的页面,但从新标签页可以访问扩展中的任何页面,只需知道扩展 ID。因此,如果发现 XSS 利用相同的参数,即使页面未在 web_accessible_resources 中配置,也可能被利用。

externally_connectable

根据 文档"externally_connectable" 清单属性声明 哪些扩展和网页可以通过 runtime.connectruntime.sendMessage 连接到您的扩展。

  • 如果在扩展的清单中 未声明 externally_connectable 键或声明为 "ids": ["*"]所有扩展都可以连接,但没有网页可以连接

  • 如果指定了 特定 ID,如 "ids": ["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"]只有这些应用程序可以连接。

  • 如果指定了 匹配项,这些网页应用程序将能够连接:

"matches": [
"https://*.google.com/*",
"*://*.chromium.org/*",
  • 如果指定为空:"externally_connectable": {},则没有应用程序或网页能够连接。

这里指示的扩展和URL越少攻击面就越小

如果在**externally_connectable中指示了易受XSS或接管攻击的网页**,攻击者将能够直接向后台脚本发送消息,完全绕过内容脚本及其CSP。

因此,这是一个非常强大的绕过

此外,如果客户端安装了一个恶意扩展,即使它不被允许与易受攻击的扩展通信,它也可以在允许的网页中注入XSS数据,或滥用**WebRequestDeclarativeNetRequest** API来操纵目标域上的请求,改变页面对JavaScript文件的请求。(请注意,目标页面上的CSP可能会阻止这些攻击)。这个想法来自这篇文章

通信总结

扩展 <--> WebApp

在内容脚本和网页之间进行通信时,通常使用后续消息。因此,在Web应用程序中,您通常会找到对函数**window.postMessage的调用,而在内容脚本中则有像window.addEventListener这样的监听器。然而,请注意,扩展也可以通过发送Post Message与Web应用程序通信**(因此网页应该预期到这一点),或者只是让网页加载一个新脚本。

在扩展内部

通常使用函数**chrome.runtime.sendMessage在扩展内部发送消息(通常由background脚本处理),为了接收和处理它,声明一个监听器调用chrome.runtime.onMessage.addListener**。

也可以使用**chrome.runtime.connect()来建立持久连接,而不是发送单个消息,可以像以下示例一样发送接收****消息**:

chrome.runtime.connect() 示例

```javascript var port = chrome.runtime.connect();

// Listen for messages from the web page window.addEventListener("message", (event) => { // Only accept messages from the same window if (event.source !== window) { return; }

// Check if the message type is "FROM_PAGE" if (event.data.type && (event.data.type === "FROM_PAGE")) { console.log("Content script received: " + event.data.text); // Forward the message to the background script port.postMessage({ type: 'FROM_PAGE', text: event.data.text }); } }, false);

// Listen for messages from the background script port.onMessage.addListener(function(msg) { console.log("Content script received message from background script:", msg); // Handle the response message from the background script });

</details>

也可以从后台脚本向位于特定标签页中的内容脚本发送消息,调用 **`chrome.tabs.sendMessage`**,在这里你需要指明要发送消息的 **标签页 ID**。

### 从允许的 `externally_connectable` 到扩展

**在 `externally_connectable` 配置中允许的 Web 应用和外部浏览器扩展** 可以使用以下方式发送请求:
```javascript
chrome.runtime.sendMessage(extensionId, ...

需要提到扩展 ID的地方。

本地消息传递

后台脚本可以与系统内的二进制文件进行通信,如果这种通信没有得到妥善保护,可能会容易受到诸如 RCE 等关键漏洞的影响稍后会详细介绍

chrome.runtime.sendNativeMessage(
'com.my_company.my_application',
{text: 'Hello'},
function (response) {
console.log('Received ' + response);
}
);

Web ↔︎ 内容脚本通信

内容脚本 操作的环境与主机页面存在的环境是 分开的,确保了 隔离。尽管存在这种隔离,双方都能够与页面的 文档对象模型 (DOM) 进行交互,这是一个共享资源。为了使主机页面能够与 内容脚本 进行通信,或通过内容脚本间接与扩展进行通信,必须利用双方都可以访问的 DOM 作为通信通道。

发送消息

content-script.js
// This is like "chrome.runtime.sendMessage" but to maintain the connection
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 === "FROM_PAGE")) {
console.log("Content script received: " + event.data.text);
// Forward the message to the background script
port.postMessage(event.data.text);
}
}, false);
example.js
document.getElementById("theButton").addEventListener("click", () => {
window.postMessage(
{type : "FROM_PAGE", text : "Hello from the webpage!"}, "*");
}, false);

安全的 Post Message 通信应检查接收到的消息的真实性,这可以通过以下方式进行检查:

  • event.isTrusted:只有在事件由用户操作触发时,这个值才为 True

  • 内容脚本可能只在用户执行某些操作时才期待接收到消息

  • origin domain:可能只允许白名单中的域名接收消息。

  • 如果使用正则表达式,请非常小心

  • Sourcereceived_message.source !== window 可用于检查消息是否来自 同一窗口,即内容脚本正在监听的窗口。

即使执行了上述检查,仍可能存在漏洞,因此请查看以下页面 潜在的 Post Message 绕过

Iframe

另一种可能的通信方式可能是通过 Iframe URLs,您可以在以下位置找到示例:

DOM

这并不是“确切的”通信方式,但 网页和内容脚本将可以访问网页 DOM。因此,如果 内容脚本 从中读取某些信息,信任网页 DOM,网页可能会 修改这些数据(因为网页不应被信任,或者因为网页易受 XSS 攻击)并 危害内容脚本

您还可以在以下位置找到一个 基于 DOM 的 XSS 以危害浏览器扩展 的示例:

内容脚本 ↔︎ 后台脚本通信

内容脚本可以使用 runtime.sendMessage() tabs.sendMessage() 发送 一次性 JSON 可序列化 消息。

要处理 响应,请使用返回的 Promise。尽管为了向后兼容,您仍然可以将 回调 作为最后一个参数传递。

内容脚本 发送请求的方式如下:

(async () => {
const response = await chrome.runtime.sendMessage({greeting: "hello"});
// do something with response here, not outside the function
console.log(response);
})();

扩展发送请求(通常是后台脚本)。以下是如何向选定标签页中的内容脚本发送消息的示例:

// From https://stackoverflow.com/questions/36153999/how-to-send-a-message-between-chrome-extension-popup-and-content-script
(async () => {
const [tab] = await chrome.tabs.query({active: true, lastFocusedWindow: true});
const response = await chrome.tabs.sendMessage(tab.id, {greeting: "hello"});
// do something with response here, not outside the function
console.log(response);
})();

接收端,您需要设置一个 runtime.onMessage 事件监听器 来处理消息。这在内容脚本或扩展页面中看起来是一样的。

// From https://stackoverflow.com/questions/70406787/javascript-send-message-from-content-js-to-background-js
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"});
}
);

在突出显示的示例中,sendResponse() 是以同步方式执行的。要修改 onMessage 事件处理程序以实现 sendResponse() 的异步执行,必须加入 return true;

一个重要的考虑是,在多个页面设置为接收 onMessage 事件的情况下,第一个执行 sendResponse() 的页面将是唯一能够有效传递响应的页面。对同一事件的任何后续响应将不被考虑。

在创建新扩展时,应该优先使用 promises 而不是回调。关于回调的使用,只有在同步上下文中直接执行 sendResponse() 函数,或者事件处理程序通过返回 true 表示异步操作时,sendResponse() 函数才被视为有效。如果没有任何处理程序返回 true,或者 sendResponse() 函数从内存中移除(被垃圾回收),则与 sendMessage() 函数关联的回调将默认被触发。

Native Messaging

浏览器扩展还允许通过 stdin 与系统中的 二进制文件 进行通信。应用程序必须安装一个 json 来指示这一点,格式如下:

{
"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/"]
}
其中 `name` 是传递给 [`runtime.connectNative()`](https://developer.chrome.com/docs/extensions/reference/api/runtime#method-connectNative) 或 [`runtime.sendNativeMessage()`](https://developer.chrome.com/docs/extensions/reference/api/runtime#method-sendNativeMessage) 的字符串,用于从浏览器扩展的后台脚本与应用程序进行通信。`path` 是二进制文件的路径,只有 1 个有效的 `type`,即 stdio(使用 stdin 和 stdout),而 `allowed_origins` 表示可以访问它的扩展(不能使用通配符)。

Chrome/Chromium 将在某些 Windows 注册表和 macOS 和 Linux 的某些路径中搜索此 json(更多信息请参见 [**docs**](https://developer.chrome.com/docs/extensions/develop/concepts/native-messaging))。

<div data-gb-custom-block data-tag="hint" data-style='success'>

浏览器扩展还需要声明 `nativeMessaing` 权限,以便能够使用此通信。

</div>

这就是一些后台脚本代码向本地应用程序发送消息的样子:
chrome.runtime.sendNativeMessage(
'com.my_company.my_application',
{text: 'Hello'},
function (response) {
console.log('Received ' + response);
}
);

这篇博客文章中,提出了一种利用本机消息的脆弱模式:

  1. 浏览器扩展对内容脚本有一个通配符模式。

  2. 内容脚本使用sendMessagepostMessage消息传递给后台脚本。

  3. 后台脚本使用sendNativeMessage将消息传递给本机应用程序。

  4. 本机应用程序以危险的方式处理消息,导致代码执行。

其中解释了如何从任何页面利用浏览器扩展进行RCE的示例。

内存/代码/剪贴板中的敏感信息

如果浏览器扩展在内存中存储敏感信息,则可能会被转储(特别是在Windows机器上)并搜索这些信息。

因此,浏览器扩展的内存不应被视为安全,并且敏感信息如凭据或助记短语不应存储

当然,不要在代码中放置敏感信息,因为它将是公开的

要从浏览器转储内存,可以转储进程内存,或者进入浏览器扩展的设置,点击**Inspect pop-up** -> 在**Memory部分 -> Take a snaphost,然后使用CTRL+F**在快照中搜索敏感信息。

此外,像助记密钥或密码这样的高度敏感信息不应允许复制到剪贴板(或至少在几秒钟内将其从剪贴板中移除),因为监控剪贴板的进程将能够获取它们。

在浏览器中加载扩展

  1. 下载浏览器扩展并解压

  2. 转到**chrome://extensions/启用**开发者模式

  3. 点击**加载已解压的扩展**按钮

Firefox中,转到**about:debugging#/runtime/this-firefox并点击加载临时附加组件**按钮。

从商店获取源代码

Chrome扩展的源代码可以通过多种方法获得。以下是每个选项的详细说明和指令。

通过命令行将扩展下载为ZIP

Chrome扩展的源代码可以通过命令行下载为ZIP文件。这涉及使用curl从特定URL获取ZIP文件,然后将ZIP文件的内容提取到一个目录中。以下是步骤:

  1. "extension_id"替换为扩展的实际ID。

  2. 执行以下命令:

extension_id=your_extension_id   # Replace with the actual extension ID
curl -L -o "$extension_id.zip" "https://clients2.google.com/service/update2/crx?response=redirect&os=mac&arch=x86-64&nacl_arch=x86-64&prod=chromecrx&prodchannel=stable&prodversion=44.0.2403.130&x=id%3D$extension_id%26uc"
unzip -d "$extension_id-source" "$extension_id.zip"

使用 CRX Viewer 网站

https://robwu.nl/crxviewer/

使用 CRX Viewer 扩展

另一种方便的方法是使用 Chrome 扩展源查看器,这是一个开源项目。可以从 Chrome Web Store 安装。查看器的源代码可在其 GitHub 仓库 中找到。

查看本地安装的扩展的源代码

本地安装的 Chrome 扩展也可以被检查。方法如下:

  1. 通过访问 chrome://version/ 并找到“Profile Path”字段来访问您的 Chrome 本地配置文件目录。

  2. 在配置文件目录中导航到 Extensions/ 子文件夹。

  3. 此文件夹包含所有已安装的扩展,通常其源代码以可读格式存放。

要识别扩展,您可以将它们的 ID 映射到名称:

  • about:extensions 页面上启用开发者模式,以查看每个扩展的 ID。

  • 在每个扩展的文件夹中,manifest.json 文件包含一个可读的 name 字段,帮助您识别扩展。

使用文件归档程序或解压缩工具

前往 Chrome Web Store 下载扩展。文件将具有 .crx 扩展名。将文件扩展名从 .crx 更改为 .zip。使用任何文件归档程序(如 WinRAR、7-Zip 等)提取 ZIP 文件的内容。

在 Chrome 中使用开发者模式

打开 Chrome 并转到 chrome://extensions/。在右上角启用“开发者模式”。点击“加载已解压的扩展...”。导航到您的扩展目录。这不会下载源代码,但对于查看和修改已下载或开发的扩展的代码非常有用。

Chrome 扩展清单数据集

为了尝试发现易受攻击的浏览器扩展,您可以使用 https://github.com/palant/chrome-extension-manifests-dataset 并检查它们的清单文件以寻找潜在的易受攻击迹象。例如,检查用户超过 25000 的扩展,content_scripts 和权限 nativeMessaing

# Query example from https://spaceraccoon.dev/universal-code-execution-browser-extensions/
node query.js -f "metadata.user_count > 250000" "manifest.content_scripts?.length > 0 && manifest.permissions?.includes('nativeMessaging')"

安全审计检查表

尽管浏览器扩展具有有限的攻击面,但其中一些可能包含漏洞潜在的加固改进。以下是最常见的: