长亭百川云 - 文章详情

新窗口创建问题 | Electron 安全

NOP Team

53

2024-07-13

0x00 简介

大家好,今天和大家讨论的是新窗口创建问题,通常来说,我们打开一个 Electron 程序,映入我们眼帘的就是主窗口,基本上是通过 BrowserWindow创建的

如果我们点击某个功能,突然在当前窗口之外跳出来一个窗口,那就是一个新窗口创建了

Electron 中,一个新窗口创建背后都意味着存在对应的管理操作,这种管理可能可以让窗口赋予非凡的权限,例如执行 Node.js

创建新窗口分为两种,一种是主进程创建的,一种是渲染进程创建的,我们今天会针对两种情况进行讨论

参考文章

https://www.electronjs.org/docs/latest/tutorial/security#14-disable-or-limit-creation-of-new-windows


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

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


  • 0x00 简介

  • 0x01 哪些情况下会创建新窗口

  • 0x02 创建新窗口带来的危害

  • 0x03 window.open 介绍

  • 1. 效果测试

  • 2. url

  • 3. frameName

  • 4. features

  • 5. 权限继承关系

  • 6. 小结

  • 0x04 window.open Node.js 测试

  • 0x05 window.open 上下文情况

  • 0x06 漏洞案例

  • 0x07 window.open 防御手段

  • 0x08 总结

  • 0x09 PDF 版 & Github

  • 往期文章


0x01 哪些情况下会创建新窗口

在之前的章节中,我们尝试过使用 BrowserWindowBaseWindow 在主进程中创建窗口,同时我们尝试过在渲染进程中通过 window.open 创建新的窗口

除此之外还有两个特例,就是 a 标签和form标签,当 a标签的 target 属性被设置为 _blank 时,点击标签会创建新窗口

form 标签渲染的表达被提交时,也会打开新窗口

除此之外的 alert 等创建的弹窗就不在讨论的范畴了

https://www.electronjs.org/zh/docs/latest/api/window-open

0x02 创建新窗口带来的危害

我们还是按照两类来说,主进程创建新窗口和渲染进程创建新窗口

主进程创建新窗口基本上都是固定的窗口,所以如果说危害,除了窗口安全配置不合理,权限分配不合理之外,如果窗口创建的配置参数中存在用户可控制的情况(这里主要是窗口加载的内容以及安全配置),可能带来一些危害

渲染进程创建新窗口在之前的文章中出现过绕过安全限制的情况(iframe + window.open) ,但 window.open 不仅仅是绕过安全限制那么简单,其实在 Electronwindow.open 是可以配置安全策略的,也就是说有可能执行 Node.js

window.open 打开的窗口配置的优先级为(向下递减)

  • webContents.setWindowOpenHandler 中指定的选项。

  • 从父窗口继承安全相关的 webPreferences

  • window.open()features 字段传入的选项

注意,webContents.setWindowOpenHandler 有最终解释权和完全权限,因为它是在主进程中调用的。

而且 window.open 也是本地文件读取漏洞的范畴内的工具之一,这个会在这篇文章中简单提到一嘴,后期出单独文章

所以今天的主角其实是 window.open

0x03 window.open 介绍

`window.open(url[, frameName][, features])   `

其中各个参数解释如下

  • url

  • frameName 名称

  • features 特性

渲染进程中的 window.open 其实相对 web 原本的 window.open 是做了一些改动的,下面我们一点一点解析

1. 效果测试

2. url

一个字符串,表示要加载的资源的 URL 或路径。如果指定空字符串("")或省略此参数,则会在目标浏览上下文中打开一个空白页

Electron 官网中对 url 参数并没有特别多的描述,但是我们搞安全的肯定得测试一下,了解其风险

1) http(s) 网址

打开 https 的网址没问题

打开 http 网站没有问题

自签名证书不行

2) file 协议加载本地文件

如果直接加载可执行二进制文件是什么效果呢?

Deepin Linux

会直接变成下载文件

Windows 11

Deepin Linux 表现一致

MacOS

报错是找不到文件,可能是将 .app 视为目录看待的

Deepin Linux 一致

3) SMB协议

刚好之前测试了 shell.openExternal ,我们顺手测试一下 smb 协议

结果比较奇怪,因为是在虚拟机中测试的 Windows ,它的行为是请求我的 MacOS 物理机打开 exe 程序,如果不在虚拟机里,会是什么样呢? 我们换一个虚拟机试一下

使用 vmware 装一个 windows 11 ,再次测试

原来是这么一个结果

4) msdt

`ms-msdt:-id PCWDiagnostic /moreoptions false /skip true /param IT_BrowseForFile="\\live.sysinternals.com\tools\procmon.exe" /param IT_SelectProgram="NotListed" /param IT_AutoTroubleshoot="ts_AUTO"   `

竟然执行成功了,虽然因为 Payload 以及系统变更的原因,导致最终执行了这么个东西,但是大家需要注意,此时是可以正常解析的,和我们之前讨论的 shell.openExternal 是一致的

所以大家要关注这类系统注册协议的安全性(URI scheme),之前就出现过 ms-officecmd 协议的注入类漏洞,这可是安全策略全开的情况下,直接从渲染进程发起的攻击

参考文章

https://blog.xlab.app/p/8fbece25/#%E6%BC%8F%E6%B4%9E%E6%8C%96%E6%8E%98

https://positive.security/blog/ms-officecmd-rce

3. frameName

frameName 其实就是原本 web 技术中 window.opentarget 属性,所以 frameName 遵循 target 的规定

一个不含空格的字符串,用于指定加载资源的浏览上下文的名称。如果该名称无法识别现有的上下文,则会创建一个新的上下文,并赋予指定的名称。

窗口的名字主要用于为超链接和表单设置目标(targets)。窗口不需要有名称。

该名称可用作 aform 元素的 target 属性

除了普通名称以外,frameName (target) 还有几个特殊的关键字:

  • _self

  • _blank

  • _parent

  • _top

这几个关键字直接理解不太好理解,我们借 a 标签来理解,这几个特殊的关键字在 a 标签中完全支持

a 标签中 target 的意义是什么呢?

该属性指定在何处显示链接的 URL,作为_浏览上下文_的名称(标签、窗口或 iframe

其实就是,我在当前页面点击了一个 a 标签,标签 href 指向的是百度的地址,你想在哪里看到点击后的结果,是当前页面呢? 还是当前页面的父页面? 还是顶级导航的页面,还是干脆新打开一个标签/窗口来展示

  • _self:当前页面加载。(a标签默认)

  • _blank:通常在新标签页打开,但用户可以通过配置选择在新窗口打开。

  • _parent:当前浏览环境的父级浏览上下文。如果没有父级框架,行为与 _self 相同。

  • _top:最顶级的浏览上下文(当前浏览上下文中最“高”的祖先)。如果没有祖先,行为与 _self 相同。

4. features

features  一个字符串,包含以逗号分隔的窗口特性列表,形式为 name=value,布尔特性则仅为 name

官方给了一个案例

`window.open('https://github.com', '_blank', 'top=500,left=200,frame=false,nodeIntegration=no')   `

在 web 技术中,这个参数叫做 windowFeatures ,但 ElectronwindowFeatures 扩充了,支持 BrowserWindowConstructorOptions 的配置,也就是构建 BrowserWindow 可以使用的配置,同时将 WebPreferences 中的一部分拿出来,也作为快捷的配置,例如

  • zoomFactor

  • nodeIntegration

  • preload

  • javascript

  • contextIsolation

  • webviewTag

具体参考

https://www.electronjs.org/zh/docs/latest/api/structures/browser-window-options

https://www.electronjs.org/zh/docs/latest/api/window-open

除了 Electron 添加的这些以外,其他配置如下

1) popup

如果启用此特性,则要求使用最小弹出窗口。弹出窗口中包含的用户界面功能将由浏览器自动决定,一般只包括地址栏。

如果未启用 popup,也没有声明窗口特性,则新的浏览上下文将是一个标签页。

备注:windowFeatures 参数中指定除 noopenernoreferrer 以外的任何特性,也会产生请求弹出窗口的效果。

要启用该特性,可以不指定 popup 值,或将其设置为 yes, 1true

例如:popup=yespopup=1popup=truepopup 的结果完全相同。

2) width 或 innerWidth

指定内容区域(包括滚动条)的宽度。最小要求值为 100

3) height 或 innerHeight

指定内容区域(包括滚动条)的高度。最小要求值为 100

4) left 或 screenX

指定从用户操作系统定义的工作区左侧到新窗口生成位置的距离(以像素为单位)

5) top 或 screenY

指定从用户操作系统定义的工作区顶部到新窗口生成位置的距离(以像素为单位)

6) noopener

如果设置了此特性,新窗口将无法通过 Window.opener 访问原窗口,并返回 null

使用 noopener 时,在决定是否打开新的浏览上下文时,除 _top_self_parent 以外的非空目标名称会像 _blank 一样处理

7) noreferrer

如果设置了此特性,浏览器将省略 Referer 标头,并将 noopener 设为 true。更多信息请参阅 rel="noreferrer"

5. 权限继承关系

  • 如果在父窗口中禁用了 Node integration, 则在打开的 window中将始终被禁用。

  • 如果在父窗口中启用了上下文隔离, 则在打开的 window 中将始终被启用。

  • 父窗口禁用 Javascript,打开的 window 中将被始终禁用

  • 非标准功能 (不由 Chromium 或 Electron 提供) 给定 features 将传递给注册 webContentsdid-create-window 事件处理函数的 options 参数。

  • 当打开 about:blank 时,子窗口的 WebPreferences 将从父窗口复制,并且没有办法覆盖它,因为Chromium在这种情况下跳过浏览器侧导航。

6. 小结

从 web 技术对于 window.open 的描述以及它的相关属性来看其实 window.open 并不等同于打开新窗口,更加准确的描述应该是 用指定的名称将指定的资源加载到新的或已存在的浏览上下文(标签、窗口或 iframe)中

打开的地址可以是 http(s) 这种web地址,也可以是本地路径和其他协议的地址,如果攻击者能够控制 url ,是可能结合 URI scheme 方面的漏洞实现全安全策略下渲染进程发起的 RCE

所以 target 属性就是指定你加载的资源要在哪个窗口(标签或 iframe) 中加载并显示,如果设置 _blank 就会打开新窗口,如果 target 的值指向一存在的窗口名字就会复用窗口

根据 web 技术中对 window.open 的描述,也和之前 web 嵌入章节一样,如果父窗口和子窗口同源,则可以通过对象关系进行访问,不同源则不行

当然,在 features 中也有 noopener 这种特性会破坏这种引用关系

参考文章

https://developer.mozilla.org/en-US/docs/Web/API/Window/open#parameters

https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/a#target

0x04 window.open Node.js 测试

按照官方文档,只有当父窗口具备 Node.js 能力时,window.open 设置了相关安全策略才可能获取到 Node.js 的能力

确实可以执行 Node.js

经过测试,window.open 打开的窗口想要具备 Node.js 能力,需要父窗口开启 nodeIntegration 关闭上下文隔离,同时 window.openfeature 中配置 nodeIntegration 和上下文隔离

如果父窗口不具备 Node.js 执行能力,但是 window.open 配置了 Node.js 支持,并且 frameName 设置为一个已经存在并且具备 Node.js 能力的窗口,此时 window.open 加载的内容是否具备 Node.js能力呢?

这个实验还挺复杂的,因为我们需要模拟一个具备 Node.js 的窗口,一个不具备 Node.js的窗口,之后还要在不具备 Node.js 的窗口里 window.open ,还有最基础的主窗口

主窗口代号为 a ,加载 index.html ,需要具备 Node.js能力

主窗口创建的具备 Node.js 能力的窗口 代号为 b ,加载 b.html

主窗口创建的不具备 Node.js能力的窗口代号为 c ,加载 c.html

c 窗口使用 window.open 抢占 b 窗口,加载 w.html ,测试是否存在 Node.js 能力

执行测试

过了 2 秒后

w.html 成功抢占 b 窗口,但其权限还是继承的 c窗口,即其父窗口,无法执行 Node.js

0x05 window.open 上下文情况

父窗口调用 window.open 创建子窗口时会返回一个指向新窗口对象的引用,父窗口可以通过这个引用直接访问子窗口的上下文

同源情况下,子窗口获取父窗口上下文测试

同源情况下的访问是双向的,与之前 iframeobject 之类的没有区别

非同源情况下,按照正常来说,父窗口访问子窗口应该还是一样的

结果并不是我们想的那样,虽然有返回对象,但是获取不到子窗口的上下文

我们可以直接在子窗口上打开开发者工具,进入控制台,输出 window.opener看看是否存在内容

存在 window.opener 但是获取不到父窗口的上下文,如果此时,在子窗口使用 window.opener 对象的 open  方法再打开一个与父窗口同源的新窗口,并且获取新窗口对象,用这个对象与父窗口进行通信,会不会就可以获取到父窗口的上下文了呢?

与父窗口同源的 2.html 内容如下

`<!DOCTYPE html>   <html>     <head>     </head>     <body>       <h1>I am 2.html !</h1>     </body>   </html>   `

此时我在非同源的这个子窗口的控制台执行

`const same_origin_window = window.opener.open('./2.html')   `

失败了,但即使成功的话,这次新建的窗口与非同源的窗口之间的关系也是非同源的,其实是没啥用的,这个思路就不行,有点骑驴找驴的意思

0x06 漏洞案例

远古时期,window.open 可以通过 file:// 远程加载 html

https://github.com/electron/electron/issues/5151

比较早的版本中 window.open 出现过权限绕过的漏洞,详情参考

https://www.electronjs.org/blog/window-open-fix

14.0 版本中修复 iframe + window.open 创建新窗口绕过安全策略漏洞

electrovolt 的文章中,在进行 Discord RCE 时,使用 window.open 绕过了沙箱,具体操作是 window.open 加载和 Discord 同源或者允许的网页地址,之后立即通过 .location 属性修改当前页面的 url 为恶意地址,实现绕过沙箱加载恶意页面

https://blog.electrovolt.io/posts/discord-rce/

任意文件读取

在这个案例中,window.open 只是一个小工具,用 iframe 等标签也可以做到,简单来说就是 window.open 支持打开本地文件,大部分程序是通过本地文件创建主窗口的,那刚好同源,就可以通过 window.open 的返回对象,获取到读取的内容,之后通过 javascript 传递给攻击者,我们通过 alert 来证明我们可以获取到值

0x07 window.open 防御手段

window.open 执行时是会触发 web-contents-created 事件的  ,所以可以在主进程对该事件进行监听,之后进行有效处理

官方给出了一个案例

`const { app, shell } = require('electron')      app.on('web-contents-created', (event, contents) => {     contents.setWindowOpenHandler(({ url }) => {       // 在这个例子中,我们要求操作系统       // 在默认浏览器中打开此事件的URL       //       // 关于哪些URL应该被允许通过shell.openExternal打开,       // 请参照以下项目。       if (isSafeForExternalOpen(url)) {         setImmediate(() => {           shell.openExternal(url)         })       }          return { action: 'deny' }     })   })   `

这个案例检查的是 url 是否符合规定,如果如何就使用 shell.openExternal 进行打开,不符合就阻止,阻止 window.open 的方法是返回 { action: 'deny' }

我们测试一下,是否能够监听到 window.open ,我们就用一个最简单的,主进程控制台打印 url ,之后拒绝创建新窗口

果然,监听到了,主进程控制台打印了 url ,并且没有新窗口创建

如果 window.openframeName(target) 设置分别设置为 _self_blank_parent_top 都会被监听并拦截吗?

对于 _self 没有监听和拦截效果

对于 _blank 具备监听和拦截效果

对于 _parent 没有监听和拦截

_top 没有拦截

如果开发者只关注新创建窗口(_blank)了,没有关注其他 frameNamewindow.open 可能会有一些遗漏,但这些遗漏会造成危害吗?

我们测试一下遗漏的几种 frameName(target) 是否可以配置执行 Node.js

_self 可以执行 Node.js,经过测试,_parent_top 也是可以的

其实这里 window.open 不设置 'nodeIntegration=true, contextIsolation=false' 也是可以执行的,毕竟是继承父窗口的权限嘛

由于这部分是新窗口创建,而当 frameName(target)设置为 _self_parent_top 都属于是导航范畴,所以Electron 官网给出上面的关于新窗口监听和拦截案例对其是无效的,可以需要参照 Electron 中关于导航相关的代码

`const { URL } = require('url')   const { app } = require('electron')      app.on('web-contents-created', (event, contents) => {     contents.on('will-navigate', (event, navigationUrl) => {       const parsedUrl = new URL(navigationUrl)          if (parsedUrl.origin !== 'https://example.com') {         event.preventDefault()       }     })   })   `

这样在 frameName(target)设置为 _self_parent_top  时就会被监听和拦截了

经过测试发现, frameName(target)设置为 _blank 时也会触发 'will-navigate' 事件,但导航事件可能在其他功能中使用到,所以开发者应该同时监听新窗口创建和导航,做更精细化地管理

a 标签和 form 标签设置 target="_blank" 时会被监听和拦截吗?

点击链接后,控制台打印要加载的地址,没有新窗口创建,也没有执行 Node.js'web-contents-created' 事件成功监听并拦截 a 标签创建新窗口的行为

action 的值设置为 allow ,即允许创建窗口

发现 a 标签通过 target="_blank" 打开的新窗口并没有继承渲染进程的能力,执行不了 Node.js

经过测试, form 标签也是一样

现在我们再来看之前 electrovolt 这种 window.open().location payload

通过 window.open 打开一个官方地址,frameName 名称不是特殊的名称,会创建新窗口或者利用旧窗口,之后立即跳转到恶意地址

如果使用的是 'web-contents-created' 事件监听,应该是可以拦截的

当然,这是 Electron 30.0 版本了,在 10.0.0 版本,代码都会报错,而且据文章描述, Discord 用的是 new-window 事件进行监听的,具体如何做的校验文章也没有描述

具体可以参考以下链接

https://www.electronjs.org/zh/docs/latest/tutorial/security#14-%E7%A6%81%E7%94%A8%E6%88%96%E9%99%90%E5%88%B6%E6%96%B0%E7%AA%97%E5%8F%A3%E5%88%9B%E5%BB%BA

https://www.electronjs.org/zh/docs/latest/api/window-open

0x08 总结

本篇文章主要是讨论创建新窗口带来的一些危害,测试主要是用的最新版本 Electron ,我们将创建新窗口分为两类

  • 主进程创建新窗口

  • 渲染进程创建新窗口

其中主进程创建新窗口可讨论的内容较少,除非攻击者可以控制构造过程中的参数,不然很难发起攻击,大部分都是写死的

渲染进程创建新窗口又可以分为两类

  • window.open 打开窗口

  • a 标签和 form标签设置 target="_blank" 打开新窗口

其中 a 标签和 form 标签打开新窗口并不能执行 Node.js ,危害不是很大

window.open 则不同,它打开或重用的窗口默认会继承父窗口的权限,也就是说如果从渲染进程调用 window.open ,恰巧渲染进程具备执行 Node.js 的能力,那么新打开或重用的窗口也会具备 Node.js 的能力,除非显式地设置 features ,限制其能力

在上下文方面,window.open 表现与之前的 iframe等基本一致,父子窗口同源情况下可以通过引用获取上下文,非同源就需要 IPC 通信了

window.open 不支持打开自签名证书的 https 网站

官方建议不用 window.open ,同时也给出了一些事件来监听新窗口的创建,app 对象监听 web-contents-created 事件可以监听到 window.open 的行为

当创建新窗口时,并可以自定义验证过程,通过设置 contents.setWindowOpenHandler 决定是否创建,

但是如果 frameName(target) 设置为 _self_parent_top ,则 window.open 的行为会变成导航行为,此时设置 contents.setWindowOpenHandler 就不管用了,导航后的窗口也是继承父窗口权限,会在 web-contents-created 事件内部的 will-navigate 监听到,并在其中进行处理

web-contents-createda 标签和 form 标签同样有监听和拦截作用,可以使用 contents.setWindowOpenHandler 进行处理

开发者在做校验时,需要考虑到 window.open(xxx).location 这种情况,做有效验证

0x09 PDF 版 & Github

PDF 版

https://pan.baidu.com/s/19p8S6NzifWY8JgVt1tKIJQ?pwd=af2g

Github

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

往期文章

有态度,不苟同

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2