前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >预加载脚本 | Electron 安全

预加载脚本 | Electron 安全

作者头像
意大利的猫
发布2024-04-22 12:39:19
920
发布2024-04-22 12:39:19
举报
文章被收录于专栏:漫流砂漫流砂

0x00 提醒

之前的一篇Electron 安全与你我息息相关文章非常的长,虽然提供了 PDF 版本,但还是导致很多人仅仅是点开看了一下,完读率大概 7.95% 左右,但上一篇真的是我觉得很重要的一篇,对大家了解 Electron 开发的应用程序安全有帮助,与每个人切实相关 但是上一篇文章内容太多,导致很多内容粒度比较粗,可能会给大家造成误解,因此我们打算再写一些文章,一来是将细节补充清楚,二来是再次来呼吁大家注意Electron 安全这件事,如果大家不做出反应,应用程序的开发者是不会有所行动的,这无异于在电脑中埋了一些地雷

我们公众号开启了留言功能,欢迎大家留言讨论~

这篇文章也提供了 PDF 版本及 Github ,见文末

0x01 简介

相信看了前面的文章,大家对于预加载脚本已经非常了解了,对于之前篇章中已经测试并解释清楚的部分,不会再次详细解释

预加载脚本 (Preload) 是一个比较让我意外的内容,可能因为学习 Electron 时就使用了官网推荐的安全开发案例,所以一直以为预加载脚本的 Node.js 就是被限制过的,但是随着最近的几篇文章的实验发现并不是

sandbox 没有被设置为 true 时(Electron 20.0 版本开始默认值为 true) ,预加载脚本是拥有完整 Node.js 环境的,如果在 Preload 中如果定义并暴露了不安全的方法,而开发者对于预加载脚本的能力并不了解可能会带来危害

0x02 预加载脚本中的Node.js

https://www.electronjs.org/zh/docs/latest/tutorial/tutorial-preload

预加载脚本的意义在于完成主进程和渲染进程之间的联络,因此重要逻辑不应该在预加载脚本中进行,也不应该赋予其过于繁重的责任,完成主进程与渲染进程之间的通信,将通信结果传递给另一方才是它实际的意义,通过暴露方法使这种固定的逻辑可以被渲染进程调用

因此预加载脚本在渲染器加载网页之前注入,也就是说预加载脚本中的内容会先一步定义好,以供网页中的 JavaScript 正确调用

如果没有被沙盒化,预加载脚本肯定是可以任意调用模块的,但是如果被沙盒化后,预加载脚本还可以加载哪些模块呢?

events

https://nodejs.org/api/events.html

这个模块是 Node.js 中关于事件处理的模块,是发布、订阅模式的实现,这里允许预加载脚本使用应该是为了让预加载脚本具备事件处理相关的能力,从预加载脚本的职责来看,也确实可能用的到

使用案例如下

代码语言:javascript
复制
const { EventEmitter } = require('events');
class MyEmiter extends EventEmitter{};
const myEmitter = new MyEmiter();

myEmitter.on('output', () => {
  console.log('hello world');
});

myEmitter.emit('output');

https://juejin.cn/post/7038496188965847070

timers

https://nodejs.org/api/timers.html

这是一个 Node.js 的定时器模块,这个模块公开了一个全局 API,用于调度在将来某个时间段调用的函数。因为计时器函数是全局函数,所以不需要调用 require('timers')来使用API。

我列几个函数大家肯定会比较熟悉

  • setImmediate
  • setInterval
  • setTimeout

还有上面对应的取消操作

  • clearImmediate
  • clearInterval
  • clearTimeout

这几个函数都是决定一段逻辑在什么时候执行,怎么执行, setImmediate 是在当前事件循环迭代结束时立即执行;setTimeout 指定时间后执行,setInterval是定期执行

比较容易表现的肯定是 setInterval ,我们让控制台每隔 3 秒打印一下时间

代码语言:javascript
复制
// preload.js

const { setInterval, clearInterval } = require('timers');

function printCurrentTime() {
  const now = new Date(); // 获取当前日期和时间对象
  const formattedTime = now.toLocaleString(); // 格式化当前时间
  console.log(`Current time: ${formattedTime}`);
}

// 每隔 3 秒打印一次当前时间
const intervalId = setInterval(printCurrentTime, 3000);

// 当不再需要定时打印时,记得清除定时器以防止内存泄漏
// 例如,可以在窗口关闭时取消定时器
window.addEventListener('beforeunload', () => {
  clearInterval(intervalId);
});

url

https://nodejs.org/api/url.html

顾名思义,用来处理 url 相关功能的模块

  • new URL()
  • format()
  • resolve()

以解析一个 url 为例

代码语言:javascript
复制
// preload.js

const url = new URL('https://example.com/path/to/resource?query=param&another=value');
console.log(url);

接下来应该是一些原本渲染进程没有或不完整而补充进来的一些方法

Buffer

https://nodejs.org/api/buffer.html

Buffer 对象用于表示固定长度的字节序列,这个模块应该是用来处理渲染页面与二进制数据交互的场景

例如

代码语言:javascript
复制
// preload.js

const { contextBridge } = require('electron');

contextBridge.exposeInMainWorld('myAPI', {
  bufferFromUtf8: (str) => Buffer.from(str, 'utf8'),
  bufferToString: (buf, encoding = 'utf8') => buf.toString(encoding),
  // ... 其他 Buffer 相关方法
});

// 在渲染进程中,可以通过 window.myAPI 来访问预加载脚本提供的方法

我看很多 V8 漏洞的Payload 都会使用到 Buffer ,看起来似乎是与二进制数据处理离不开的模块

process

https://www.electronjs.org/zh/docs/latest/api/process

这个模块用来处理对象的扩展,官方的案例中获取 ElectronNode.jsChromium版本就是使用的这个模块,可以通过 process 模块获取一些信息,具体如下

  • crash()
  • hang()
  • getCreationTime()
  • getHeapStatistics()
  • getBlinkMemoryInfo()
  • getProcessMemoryInfo()
  • getSystemMemoryInfo()
  • getSystemVersion()
  • getCPUUsage()
  • getIOCounters()
  • uptime()
  • argv
  • execPath
  • env
  • pid
  • arch
  • platform
  • 沙盒化
  • contextIsolated
  • type
  • version
  • versions
  • mas
  • windowsStore
  • contextId

直接打印 process 可以直接看到它的属性和方法

setImmediate

上面已经提到

clearImmediate

上面已经提到

electron

  • contextBridge
  • crashReporter
  • ipcRenderer
  • nativeImage
  • webFrame
  • webUtils

这里的 contextBridge 是用来向渲染进程暴露变量/常量和函数的方法,在下面的部分详细介绍;ipcRendererPreload 脚本用来和主进程进行 IPC 通信的工具,我们详细看看剩下几个是干嘛的

crashReporter

将崩溃日志提交给远程服务器 https://www.electronjs.org/zh/docs/latest/api/crash-reporter

代码语言:javascript
复制
const { crashReporter } = require('electron')

crashReporter.start({ submitURL: 'https://your-domain.com/url-to-submit' })

如何构建崩溃日志收集系统可以点击上方的链接

nativeImage

使用 PNG 或 JPG 文件创建托盘、dock和应用程序图标。 https://www.electronjs.org/zh/docs/latest/api/native-image

代码语言:javascript
复制
const { BrowserWindow, Tray } = require('electron')

const tray = new Tray('/Users/somebody/images/icon.png')
const win = new BrowserWindow({ icon: '/Users/somebody/images/window.png' })

这个在开发过程中就会遇到,但不太理解为什么开放给 preload

webFrame

自定义渲染当前网页

这个不难理解,如果不通过 webFrame 也可以通过 DOM 等操纵网页

例如将当前页缩放到200% 的示例

代码语言:javascript
复制
const { webFrame } = require('electron')
webFrame.setZoomFactor(2)

很方便,确实大了

webUtils

与Web API对象(文件、Blob等)交互的实用程序层 https://www.electronjs.org/zh/docs/latest/api/web-utils

例如获取文件路径

代码语言:javascript
复制
const { webUtils } = require('electron')
const newPath = webUtils.getPathForFile(document.querySelector('input').files[0])

小提醒

可以关注一下这几个模块的漏洞通告,如果出现漏洞,可能会影响到 Electron

0x03 风险点

Preload 可以说是平衡风险和便捷的一种措施,本身已经做得不错了,风险点也都是开发者不安全编码造成的

  • 未开启上下文隔离及 sandbox
  • 不安全的实现
  • 接口过度暴露

第一点就不多说了,前面的文章已经说清楚了,主要说后面两点,在后面两点中,我们的前提是开启了上下文隔离,开启了 sandbox

1. 不安全的实现

开启了安全措施后, Preload自己是很难造成大的问题,主要是配合主进程,举个极端一些的例子

渲染进程可以读取 docs 目录下的文件,文件名由调用者提供,preload.js 与主进程通信,读取并返回内容

main.js

代码语言:javascript
复制
const { app, BrowserWindow, ipcMain } = require('electron');
const fs = require('fs');
const path = require('path')

function createWindow() {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
    },
  });

  win.loadFile('./index.html');
}

app.whenReady().then(() => {
  ipcMain.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);
      return null;
    }
  });
  createWindow();
});

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

preload.js

代码语言:javascript
复制
const { 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);
      return null;
    }
  },
});

index.html

代码语言:javascript
复制
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Electron Path Traversal Vulnerability Demo</title>
</head>
<body>
  <input type="text" id="fileNameInput" placeholder="Enter file name">
  <button id="readFileButton">Read File</button>

  <pre id="fileContent"></pre>

  <script src="./renderer.js"></script>
</body>
</html>

renderer.js

代码语言:javascript
复制
const fileNameInput = document.getElementById('fileNameInput');
const readFileButton = document.getElementById('readFileButton');
const fileContent = document.getElementById('fileContent');

readFileButton.addEventListener('click', async () => {
    const fileName = fileNameInput.value;
    const data = await window.myApi.readFile(fileName)
    // console.log(data)
    fileContent.textContent = data || 'No content available.'
});

我们输入要访问的文档的名字 readme.txt

此时预加载脚本没有做安全检查,将文件名称直接拼接传递给主进程,因此如果我们输入 ../../../../../../etc/passwd 这种名称,就可能导致任意文件读取漏洞

这种属于是不安全的实现,案例比较极端,但是意思应该表达清楚了,这属于是 Preload 和主进程实现上做得不安全,导致问题

2. 过度暴露

在上面的例子中,我们使用了 Electron 30.0.0 版本,开启了 sandbox ,使用预加载脚本使用 contextBridgeAPI 暴露给渲染进程,我们将打开文件功能进行了封装,封装成了一个函数,这也就意味着每个新功能,如果需要主进程参与可能都会创建不止一个新的函数

如果开发者直接将 ipcRendereripcRenderer.invoke 这种 API或非必要函数直接暴露给渲染进程,就可能导致渲染进程任意发起 IPC 通信、获取敏感信息等

假设程序有很多和操作系统命令执行结果相关的功能,所以主进程有一个接收参数并执行的通信,这样 Preload 脚本中直接传递参数,复用这一个监听即可

main.js

代码语言:javascript
复制
const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('path');

function createWindow() {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
    },
  });

  win.loadFile('./index.html');
}

app.whenReady().then(() => {
  ipcMain.handle('exec-command', async (event, cmd) => {
    return new Promise((resolve, reject) => {
      require('child_process').exec(cmd, (error, stdout, stderr) => {
        if (error) {
          console.error(`Error executing command "${cmd}":`, error);
          reject(error);
        } else {
          resolve(stdout.trim());
        }
      });
    });
  });

  createWindow();
});

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

可以通过与主进程 IPC 通信,之后传递要执行的命令的字符串,并通过 IPC 传递会渲染进程

此时并不能说主进程写得不安全,如果 Preload 脚本固定传递的字符,例如 cat /etc/issue 便不会出现任意命令执行

如果此时渲染进程和 Preload 如下,则就会产生风险

index.html

代码语言:javascript
复制
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Electron Path Traversal Vulnerability Demo</title>
</head>
<body>
  <pre id="cmdResultContent"></pre>

  <script src="./renderer.js"></script>
</body>
</html>

preload.js

代码语言:javascript
复制
const { contextBridge, ipcRenderer } = require('electron');

// 错误地直接通过 contextBridge 将整个 ipcRenderer 对象暴露给渲染进程
contextBridge.exposeInMainWorld('electronApi', {
  invoke: ipcRenderer.invoke,
});

renderer.js

代码语言:javascript
复制
const fileContent = document.getElementById('cmdResultContent');

window.electronApi.invoke('exec-command', 'pwd').then((result) => {
    fileContent.textContent = result || 'No cmd exec result available.'
})

此时就会导致任意命令执行

0x04 总结

预加载脚本的风险主要来源于不安全的编码习惯,但是有些泄漏可能是不容易发现的,例如有几个函数只是给 Preload 自己使用的,但是不小心暴露给了渲染进程;函数是给自己写的渲染进程使用的,结果同时暴露给了 iframe 这种嵌入内容等

预加载脚本是一个很好的代码审计的切入点,如果安全配置较为完善,则安全漏洞的利用基本都要通过预加载脚本传递数据,也就是掌握了咽喉位置,详细分析每一个 IPC 通信,就能找到几乎所有渲染进程攻击主进程的攻击面

0x05 PDF 版本 & Github

PDF 版本

https://pan.baidu.com/s/1lz6_Wre86KxusMsQFcyjUA?pwd=w3yh

Github

https://github.com/Just-Hack-For-Fun/Electron-Security

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-04-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 NOP Team 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 0x00 提醒
  • 0x01 简介
  • 0x02 预加载脚本中的Node.js
    • events
      • timers
        • url
          • Buffer
            • process
              • setImmediate
                • clearImmediate
                  • electron
                    • crashReporter
                    • nativeImage
                    • webFrame
                    • webUtils
                  • 小提醒
                  • 0x03 风险点
                    • 1. 不安全的实现
                      • 2. 过度暴露
                      • 0x04 总结
                      • 0x05 PDF 版本 & Github
                      相关产品与服务
                      代码审计
                      代码审计(Code Audit,CA)提供通过自动化分析工具和人工审查的组合审计方式,对程序源代码逐条进行检查、分析,发现其中的错误信息、安全隐患和规范性缺陷问题,以及由这些问题引发的安全漏洞,提供代码修订措施和建议。支持脚本类语言源码以及有内存控制类源码。
                      领券
                      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档