Electron简介
本文参考大佬文章[1],摘取部分个人认为比较重要的内容,产出了这篇文章,算是一个学习笔记吧。
Electron是一个使用Javascript、HTMl和CSS构建桌面应用程序的架构[2]。嵌入Chromium
和Node.js
到二进制的Electron允许您保持一个Javascript代码代码库并创建在Windows上运行的跨平台应用macOS和Linux一一不需要本地开发经验。
Electron架构
Chromium
具备网页渲染能力,Nodejs
具备操作系统API的能力。
因此从架构上,Electron
分为两个部分:主进程和渲染进程
主进程
每个Electron应用都有一个单一的主进程,作为应用程序的入口点。主进程在Node.js
环境中运行,这意味着它具有require
模块和使用所有Node.js API的能力。
渲染进程
每个Electron应用都会为每个打开的BrowserWindow
(与每个网页嵌入)生成一个单独的渲染器进程。恰如起名,渲染器负责渲染网页内容。所以实际上,运行于渲染器进程中的代码是须遵照网页标准的(至少就目前使用的Chromium而言是如此)。
预加载脚本(preload)
主进程可以与操作系统交互,渲染进程只能渲染网页,那么当功能需要操作系统支持的时候,渲染进程如何将需求传递给主进程,主进程又如何将结果传递给渲染进程就是个问题!
Electron设计了一系列的IPC
功能,方便主进程和渲染进程间通信,渲染进程的通信通常在preload
脚本中发生。
预加载(preload
)脚本包含了那些执行于渲染器进程中,且先于网页内容开始加载的代码。这些脚本虽运行于渲染器的环境中,却因能访问Node.js API而拥有了更多的权限。当然,为了安全考虑,它的API是受限的,主要就是发起IPC
请求或监听,将自定义的API
和变量等传递给渲染进程使用。
实用进程
在Electron 22.0.0
中开始引入utility process
[3],每个Electron应用程序都可以使用主进程生成多个子进程UtilityProcess API
,实用进程(官方翻译叫效率进程)可用于托管,例如:不受信任的服务器,CPU密集型任务或以前容易崩溃的组件托管在主进程或使用Node.jschild_process.fork
API生成的进程中。
远程调试Electron
很多情况下,Electron
应用都是无法打开控制台的,这就给测试增加了麻烦。通过这个远程调试的方法,可以远程打开控制台对应用进行控制台调试,更加方便。
以"夸克"为例:
1 | cd /Applications/Quark.app/Contents/MacOS |
然后Chrome浏览器访问chrome://inspect/#devices
点击inspect
就可以了。
涉及安全问题的配置
想要去挖掘到Electron开发的应用漏洞,就要从下面的几个参数配置下手。
nodeIntegration[6]
这个属性是主进程创建渲染进程窗口控制渲染进程是否具备执行NodeJs
的能力,如果该属性设置为true
,渲染进程一旦出现XSS
等漏洞,能够执行Javascript
代码,就会导致RCE
漏洞。
这个特性在5.0
版本开始默认设置为false
,Electron
中通过IPC
通信,可以让渲染进程执行开发者自定义的功能,这种通信更加安全,绝不推荐开启nodeIntegration
功能。
contextIsolation[7]
上下文隔离,从Electron 12.0.0
版本开始默认开启(true
)
这个属性是主进程创建渲染进程窗口控制渲染进程是否具备覆盖JavaScript
方法的能力,如果该属性设置为false
,渲染进程一旦出现XSS
等漏洞,能够执行覆盖Javascript
代码,配合预加载脚本及主进程定义好的功能,可能会导致RCE
漏洞。
Preload预加载脚本[8]
简单粗略来说,Electron = Nodejs + Chromium
,Nodejs
负责系统交互,可以理解为主进程,Chromium
负责页面渲染,可以理解为渲染进程。
比较常见的配置是禁止渲染进程执行Node.js
代码,同时开启上下文隔离,此时如果渲染进程想要使用操作系统或者硬件的部分功能怎么办?
**通过IPC
通信!!!**主进程定义好功能,之后渲染进程只传递数据,这样就可以保证XSS
不会轻易导致RCE
。但是如果所有渲染进程都可以发IPC
也很危险,所以在主进程和渲染进程之间吧,设置了一种叫preload预加载脚本的东西,会在创建窗口的时候指定预加载脚本可以执行部分Node.js
代码。
毕竟,preload要代表渲染进程与主进程通信,但是危险的方法都用不了,之后通过一种官方定义的桥将方法暴露给渲染进程,这样渲染进程直接调用方法就好了。
检查预加载preload到底有没有漏洞,主要看两点:
- 是否存在危险方法:
- 加载任意
Nodejs
模块等。
- 加载任意
- 是否存在过度暴露
- 例如预加载脚本preload将用来
IPC
通信的对象直接暴露给渲染进程。
- 例如预加载脚本preload将用来
预加载脚本文件检查过程中要与主进程关联着看,IPC
通信是有暗号
的,一一对应。
官方实例:渲染进程到主进程(双向)
我们来看一个官方的案例[9]。
其中的模式2:渲染器进程到主进程(双向)
这是一个渲染器进程到主进程通信的案例,我们将从渲染器进程打开一个原生的文件对话框,并返回所选文件的路径。
可以看到主进程创建窗口前,先试用ipcMain.handle()
创建了监听,并提供了"暗号",即代码中的dialog:openFile
,以及对应的处理程序。
预加载脚本preload.js
通过contextBridge
向渲染进程暴露了方法electronAPI.openFile()
方法就可以了,调用后,预加载脚本中ipcRenderer
向主进程发送“暗号”dialog:openFile
,主进程就明白过来了,交给handleFileOpen
函数进行处理。
回到我们刚才提到的两点检查项:
-
是否存在危险方法?
如果有一些方法是预加载脚本传递命令字符串,主进程负责执行并返回,那这就属于危险方法了。
-
是否存在过度暴露?
如果预加载脚本直接将
ipcRenderer
暴露给渲染进程,而不是上面的electronAPI.openFile()
,那就属于过度暴露。
如果上下文隔离被设置为false
,那渲染进程里写的内容就已经没有那么重要了,因为每一个方法都可能被渲染进程覆盖,只能看主进程的实现上有没有危险的内容。
Sandbox[10]
Chromium的一个关键安全特性是,进程可以在沙盒中执行。沙盒通过限制对大多数系统资源的访问来减少恶意代码可能造成的伤害 — 沙盒化的进程只能自由使用CPU
周期和内存。为了执行需要额外权限的操作,沙盒处的进程通过专用通信渠道将任务下放给更大权限的进程[11]。
从Electron 20
开始,渲染进程默认启用了沙盒,无需进一步配置。如果你想禁用某个进程的沙盒,请参阅为单个进程禁用沙盒部分。
官方文档中还说了在渲染器中启用 nodeIntegration 时,沙盒也会被禁用。
自定义协议[12]
这个部分是经常产生漏洞的地方,检查也就是看其是否合理,有没有目录浏览,目录穿越等,导致的问题主要是本地文件泄露和远程代码执行。
webSecurity[13]
webSecurity
是开启同源策略的,默认即开启。
关闭webSecurity
可能导致加载其它域的JavaScript
脚本。
内容安全策略CSP[14]
内容安全策略(CSP)是应对XSS
攻击和数据注入攻击的又一层保护措施。我们建议任何载入到Electron
的站点都要开启。
CSP属于是一种白名单机制,能够有效的防止外部JavaScript
注入执行等,建议开启,检查方法也比较简单,就看窗口加载的html中是否设置了策略即可。
相关的配置类似如下:
1 | <meta http-equiv="Content-Security-Policy" content="default-src 'none'"> |
针对v37.2.6各种配置的实际危害测试
Electron
官方开发了Electron Fiddle
程序,可以直接选择Electron
版本,非常方便,但是需要系统准备对应的NodeJS
环境,代码就使用默认的,我们在其中修改配置,进行测试。
测试从一下几个角度进行:
安全配置 | 测试执行JS点 |
---|---|
nodeIntegration | 渲染的页面 |
contextIsolation | 预加载脚本(Preload) |
sanbox | iframe内页面 |
3个配置选项,2 x 2 x 2 = 8种结果,true
开启,false
关闭,分布如下:
配置序号 | nodeIntegration | contextIsolation | sandbox |
---|---|---|---|
1 | true | true | true |
2 | true | true | false |
3 | true | false | true |
4 | true | false | false |
5 | false | true | true |
6 | false | true | false |
7 | false | false | true |
8 | false | false | false |
测试环境和payload
本次测试的的Electron
的版本为:v37.2.6
,使用的系统是Macos
。
测试的Payload:
1 | require('child_process').exec('open -a Calculator') |
iframe服务器attack.com/1.html
1 |
|
window.open服务器attack.com/2.html
1 |
|
attack.com/3.html
1 |
|
为了测试iframe
,关闭CSP
。
默认配置(配置5)
根据官方文档Electron
自5.0.0.
以后,nodeIntegration
默认是为false
,自12.0.0
以后contextIsolation
默认为true
。从 Electron 20
开始,渲染进程默认启用了沙盒。CSP官方给的案例中是使用了的,这里为了测试iframe,要给CSP去掉。
所以这里的配置为:
1 | nodeIntegration: false |
即这里就是前面表格中所说的配置5的环境。
下面是测试的结果。
预加载脚本preload
运行,发现没弹计算器,失败。
渲染进程
运行,发现没弹计算器,失败。
iframe
运行,发现没弹计算器,失败。
小结
测试执行JS点 | 是/否可以执行NodeJS |
---|---|
预加载脚本preload | 否 |
渲染页面 | 否 |
iframe | 否 |
iframe + window.open | 否 |
配置1
配置情况:
1 | nodeIntegration: true |
预加载脚本preload
运行,发现没弹计算器,失败。
渲染进程
运行,发现没弹计算器,失败。
iframe
运行,发现没弹计算器,失败。
小结
测试执行JS点 | 是/否可以执行NodeJS |
---|---|
预加载脚本preload | 否 |
渲染页面 | 否 |
iframe | 否 |
iframe + window.open | 否 |
配置2
配置情况:
1 | nodeIntegration: true |
预加载脚本preload ✔️
运行,成功弹出计算器!!!
渲染脚本
运行,发现没弹计算器,失败。
iframe
运行,发现没弹计算器,失败。
小结
测试执行JS点 | 是/否可以执行NodeJS |
---|---|
预加载脚本preload | 是!!! |
渲染页面 | 否 |
iframe | 否 |
iframe + window.open | 否 |
配置3
配置情况:
1 | nodeIntegration: true |
预加载脚本preload
运行,发现没弹计算器,失败。
渲染脚本
运行,发现没弹计算器,失败。
iframe
运行,发现没弹计算器,失败。
小结
测试执行JS点 | 是/否可以执行NodeJS |
---|---|
预加载脚本preload | 否 |
渲染页面 | 否 |
iframe | 否 |
iframe + window.open | 否 |
配置4
配置情况:
1 | nodeIntegration: true |
预加载脚本preload ✔️
运行,成功弹出计算器!!!
渲染脚本 ✔️
运行,成功弹出计算器!!!
iframe
运行,发现没有弹计算器,失败。
小结
测试执行JS点 | 是/否可以执行NodeJS |
---|---|
预加载脚本preload | 是!!! |
渲染页面 | 是!!! |
iframe | 否 |
iframe + window.open | 否 |
配置6
配置情况:
1 | nodeIntegration: false |
预加载脚本preload ✔️
运行,成功弹出计算器!!!
渲染脚本
失败,未成功弹出计算器。
iframe
失败,未成功弹出计算器。
小结
测试执行JS点 | 是/否可以执行NodeJS |
---|---|
预加载脚本preload | 是!!! |
渲染页面 | 否 |
iframe | 否 |
iframe + window.open | 否 |
配置7
配置情况:
1 | nodeIntegration: false |
预加载脚本preload
运行,失败,未弹出计算器。
渲染脚本
运行,失败,未成功弹出计算器。
iframe
运行,失败,未弹出计算器。

小结
测试执行JS点 | 是/否可以执行NodeJS |
---|---|
预加载脚本preload | 否 |
渲染页面 | 否 |
iframe | 否 |
iframe + window.open | 否 |
配置8
配置情况:
1 | nodeIntegration: false |
预加载脚本preload ✔️
运行,成功弹出计算器!!!
渲染脚本
失败,未弹出计算器。
iframe
失败,未弹出计算器
小结
测试执行JS点 | 是/否可以执行NodeJS |
---|---|
预加载脚本preload | 是!!! |
渲染页面 | 否 |
iframe | 否 |
iframe + window.open | 否 |
总结
了解了前面介绍的一些概念,针对于Electron v37.2.6
版本,我对8种的安全配置进行了测试。测试发现只要是sandbox
设置为true
的均不能成功弹出计算器!不论nodeIntegration和contextIsolation什么配置。还发现了在网页中嵌入<iframe>
然后src引入存在payload的页面,发现都没有弹计算器,不论什么配置,且iframe的src引入的页面,再使用window.open打开一个存在payload的页面,依然是不能弹计算器的。
可以得到如下的结论:
安全配置 | 预加载脚本preload | 渲染页面 | iframe |
---|---|---|---|
nodeIntegration | 不影响 | true | |
contextIsolation | 不影响 | false | |
sandbox | false | false | |
额外条件 |
这里不谈绕过和覆盖的问题!
由上述表格可以看到:
- 在预加载脚本preload中,不论
nodeIntegration
和contextIsolation
的配置是什么,只要sandbox
为false
,就可以弹计算器,即就可执行系统命令。 - 在渲染页面中,
nodeIntegration
配置为true
,contextIsolation
配置为false
,sandbox
配置为false
才可以弹计算器,执行系统命令。
有了这些,那我们作为挖掘SRC的,如何去挖掘漏洞呢?
找预加载脚本preload和渲染网页的XSS漏洞!利用XSS漏洞实现RCE!
首先大方向sandbox
要配置为false
才行,对于预加载脚本preload存在XSS,可以直接执行系统命令。对于渲染页面存在XSS漏洞,需要配置是nodeIntegration
为true
,contextIsolation
为false
,这样也可直接执行系统命令。
对于渲染页面存在XSS漏洞,sandbox
为false
,一定要nodeIntegration
为true
,contextIsolation
为false
吗?
不一定!前面我也有介绍到了IPC通信!!!主进程和渲染进程之间通信,通过预加载脚本preload进行架桥连接,若preload中存在一些危险的函数,可以执行js,也是可能存在XSS -> RCE的。
补充部分
根据大佬的文章[1:1],经过测试,在Electron 20.0
以及以后的版本并不是默认sandbox: true
,或者说并不完全等于显式地设置sandbox: true
。我在前面的测试例子中都是直接将webPreferences
配置中的sandbox
直接显示地写出来了,那如果不写这个参数呢?默认的sandbox
的配置一定是true
吗?答案是不一定!
大佬的结论是:当nodeIntegration
、nodeIntegrationInSubFrames
、nodeIntegrationInWorker
被设置为true
时,sandbox
对于Node.js
的保护效果就会失效,当contextIsolation
被设置为false
时,sandbox
对于上下文隔离的保护效果就会失效。
我的测试
我对于大佬的结论进行了测试,发现结论不够严谨。结论中并没有说明nodeIntegration
、nodeIntegrationInSubFrames
和nodeIntegrationInWorker
三个都为true
,sandbox
对于Node.js
的保护效果就会失效,还是说某几个为true
才会失效?
单独开一个nodeIntegrationInSubFrames: true
,配合contextIsolation: false
,并不会弹出计算器。
单开一个nodeIntegration: true
,配合contextIsolation: false
,即可在渲染器中实现弹出计算器。这里可以弹计算器,符合了前面说的启用了nodeIntegration
,sandbox
会被禁用。
经过测试发现,在sandbox
不配置,默认的情况下,发现只要nodeIntegration
或者nodeIntegrationInWorker
有一个为true
,preload中就可以执行系统命令。
我还发现,在下面的配置下,渲染器中的iframe
是可以弹计算器的,而且要是严格的这个配置。但是发现,window.open
是不会弹计算器的。
1 | nodeIntegration: true |
我的结论
根据上面的多个测试,在Electron v37.2.6
版本下的,我的结论如下:
对于显式的写出了sandbox
的配置,遵循测试的表格:
安全配置 | 预加载脚本preload | 渲染页面 | iframe |
---|---|---|---|
nodeIntegration | 不影响 | true | |
contextIsolation | 不影响 | false | |
sandbox | false | false | |
额外条件 |
- 在预加载脚本preload中,不论
nodeIntegration
和contextIsolation
的配置是什么,只要sandbox
为false
,就可以弹计算器,即就可执行系统命令。 - 在渲染页面中,
nodeIntegration
配置为true
,contextIsolation
配置为false
,sandbox
配置为false
才可以弹计算器,执行系统命令。
对于未写出sandbox
的配置:
对于预加载脚本preload:
sandbox
默认是为true
,nodeIntegration
或nodeIntegrationInWorker
设置为true
时,二者只要有一个true
就可,sandbox
就会被禁用,preload中就可以执行系统命令。
对于渲染页面:
在渲染页面中,nodeIntegration
为true
和contextIsolation
为false
,必须这样,渲染页面中才能执行系统命令。
在iframe
中,必须要nodeIntegration
为true
,nodeIntegrationInSubFrames
为true
和contextIsolation
为true
的情况下,渲染器的iframe
才能执行系统命令。
安全配置 | 预加载脚本preload | 渲染页面 | iframe |
---|---|---|---|
nodeIntegration | true | true | true |
nodeIntegrationInSubFrames | 不影响 | 不影响 | true |
nodeIntegrationInWorker | true | 不影响 | 不影响 |
contextIsolation | 不影响 | false | false |
额外条件 |
参考
https://github.com/Just-Hack-For-Fun/Electron-Security ↩︎ ↩︎
https://www.electronjs.org/zh/blog/electron-22-0#utilityprocess-api-36089 ↩︎
https://www.electronjs.org/zh/docs/latest/tutorial/process-model ↩︎
https://www.electronjs.org/zh/docs/latest/tutorial/security#2-不要为远程内容启用-nodejs-集成 ↩︎
https://www.electronjs.org/zh/docs/latest/tutorial/security#3-上下文隔离 ↩︎
https://www.electronjs.org/zh/docs/latest/tutorial/process-model ↩︎
https://www.electronjs.org/zh/docs/latest/tutorial/ipc#模式-2渲染器进程到主进程双向 ↩︎
https://www.electronjs.org/zh/docs/latest/tutorial/sandbox ↩︎
https://www.electronjs.org/zh/docs/latest/tutorial/security#4-启用进程沙盒化 ↩︎
https://www.electronjs.org/zh/docs/latest/api/protocol#protocolregisterschemesasprivilegedcustomschemes ↩︎
https://www.electronjs.org/zh/docs/latest/tutorial/security#6-不要禁用-websecurity ↩︎
https://www.electronjs.org/zh/docs/latest/tutorial/security#7-content-security-policy内容安全策略 ↩︎