通過拆分請求來實現的SSRF攻擊
Firefox Accounts API伺服器對unicode字元的一些錯誤處理可能允許攻擊者向其後端資料儲存發出任意請求。這就會引起一個漏洞:HTTP請求路徑中的unicode字元損壞。
漏洞的發現過程
此漏洞還是在我除錯一個不相關的unicode處理問題時發現的,該漏洞最終導致了ofollow,noindex">Node.js`http`模組 的錯誤。當使用含有 '/café:dog:, the server receives /café=6路徑的`http.get` 請求時,就會出現此錯誤。
換句話說,我要求Node.js向特定路徑發出HTTP請求,但是請求發出後,路徑卻發生了變化!在深入研究這個細節後,我發現這個問題是這樣引起的:當HTTP請求寫入進入路徑的unicode字元時,Node.js會將unicode字元損壞。
雖然使用`http`模組的使用者通常將請求路徑指定為字串,但最終Node.js還是必須將這些請求轉化為原始位元組輸出。 JavaScript具有unicode字串,因此將它們轉換為位元組,就意味著要選擇並應用適當的unicode編碼。對於不包含主體的請求,Node.js會預設使用“latin1”,這是一種單位元組編碼形式,不能編碼複雜的unicode字元,例如表情符號。但是,這些複雜的字元,可以被分解成其內部JavaScript表示的單位元組形式。
> v = "/caf\u{E9}\u{01F436}" '/café:dog:' > Buffer.from(v, 'latin1').toString('latin1') '/café=6'
在處理使用者輸入時,資料損壞常常是潛在安全問題的危險訊號,比如,在本文的案例中,通過我所用的firefox的程式碼庫發出的HTTP請求,我就知道包含使用者輸入的路徑可能會出現風險。所以我立即聯絡了安全團隊,然後根據對方提供的unicode字串尋找我可能構建了URL的位置。
通過拆分請求來實現的SSRF攻擊漏洞
經過分析,我發現這種漏洞是一種稱為拆分請求 的攻擊,而基於文字的協議(如HTTP)通常容易受到這樣的攻擊。如果有一個伺服器,它接受一些使用者輸入的內容,並將其包含在通過HTTP公開的內部服務的請求中,那整個過程就如下所示。
GET /private-api?q=<user-input-here> HTTP/1.1 Authorization: server-secret-key
如果伺服器未正確驗證使用者的輸入,則攻擊者可能會將協議控制字元直接注入到傳送請求中。假設通過這種方式,伺服器接受了使用者的以下輸入。
"x HTTP/1.1\r\n\r\nDELETE /private-api HTTP/1.1\r\n"
當發出請求時,伺服器可能會直接將其寫入路徑,如下所示:
GET /private-api?q=x HTTP/1.1 DELETE /private-api Authorization: server-secret-key
此時,接收端會將此此請求解釋為兩個獨立的HTTP請求:一個`GET`請求和一個`DELETE`請求,這樣接受端就無法知道呼叫者的真實意圖。
實際上,這種精心設計的使用者輸入將誘使伺服器發出額外的出站請求,這種情況就被稱為伺服器端請求偽造或 “SSRF”。此時,伺服器可能具有攻擊者不具有的許可權,例如訪問內部網路或祕密API金鑰,這會增加問題的嚴重性。
高質量的HTTP庫通常包括防止這種危險行為的緩解措施,Node.js也不例外。如果你嘗試使用路徑中的控制字元發出一個出站HTTP請求,則它們將在被寫入之前進行percent escape。
> http.get('http://example.com/\r\n/test').output [ 'GET /%0D%0A/test HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n' ]
不幸的是,上述處理unicode字元的錯誤則意味著可以規避這些防護措施。比如如下的URL,其中包含一些帶有附加符號的unicode字元。
> 'http://example.com/\u{010D}\u{010A}/test' http://example.com/čĊ/test
當Node.js版本8或更低版本對此URL發出`GET`請求時,這些請求是不會進行percent escape的,因為它們不是HTTP控制字元。
> http.get('http://example.com/\u010D\u010A/test').output [ 'GET /čĊ/test HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n' ]
但是當這些字串被編碼為latin1以將其寫入路徑時,這些字元將分別被拆分為 "\r"和 "\n"。
> Buffer.from('http://example.com/\u{010D}\u{010A}/test', 'latin1').toString() 'http://example.com/\r\n/test'
因此,通過在請求路徑中加入精心選擇的unicode字元,攻擊者可以欺騙Node.js將HTTP協議控制字元寫入路徑。
不過最新的Node.js 10版本修復了此漏洞,如果請求路徑包含非ascii字元,則會彈出一個錯誤警報。但是對於Node.js版本8及其以下的版本,任何發出HTTP請求的伺服器都可能受到SSRF的攻擊,其中就包括:
1.接受來自使用者輸入的unicode資料;
2.將輸入的unicode資料包含在傳出HTTP請求的請求路徑中;
3.長度為零的請求(例如GET或DELETE);
偽造對FxA資料儲存的請求
我稽核了FxA伺服器堆疊,以查詢在請求路徑中使用長度為零的內容和使用者提供的資料發出HTTP請求的位置,於是我發現可以觸發上述錯誤的三個位置。
第一個是我們對WebPush的支援,登入的客戶端可以提供一個https URI,在該URI處接收帳戶狀態更改的通知,伺服器將通過發出零長度的“PUT”請求來傳遞該通知。幸運的是,在這種情況下,伺服器發出的請求不具有任何特殊許可權或包含任何API令牌。這樣,你就可以利用這個漏洞誘使FxA伺服器向webpush通知主機發出的意外請求,但該請求不會比攻擊者直接發出的請求更強大。
第二個是檢查BrowserID證書的真實性,其中FxA伺服器會從使用者提供的JSON blob中解析主機名,然後通過發出'GET`請求來獲取該主機的簽名金鑰,如:
GET /.well-known/browserid?domain=<hostname>
我可以利用此漏洞來誘使伺服器對任意主機名發出任意請求,幸運的是,在Firefox的執行環境中,這些請求都是通過squid快取代理髮送的,該代理配置了嚴格的驗證規則來阻止任何意外的傳出請求,這就防止了這個漏洞在此種情況中被利用。
第三個是向後端資料儲存發出HTTP請求,而正是在這個位置,我發現了一個真正可利用的漏洞。
你需要了解的是,Firefox的帳戶執行伺服器分為面向Web的API伺服器和與SQL/">MySQL資料庫通訊的獨立內部資料儲存區伺服器,如下所示:
+--------++--------++-----------++----------+ | Client |HTTP|API|HTTP| DataStore |SQL|MySQL| ||<------>| Server |<------>|Service|<----->| Database | +--------++--------++-----------++----------+
API伺服器通過普通的老式HTTP與資料儲存區服務進行通訊,結果發現只有一個地方,可以讓來自使用者輸入的unicode資料進入其中一個請求的路徑。
由於我的許多資料儲存請求都是通過電子郵件地址輸入的,且電子郵件地址包含unicode字元。為了避免兩個服務之間的unicode編碼和解碼問題,我的資料儲存API中的大多數與電子郵件相關的操作都會將該電子郵件以十六進位制編碼的utf8字串接受。例如,API伺服器會通過向資料儲存發出如下HTTP請求來獲取電子郵件“[email protected]”的帳戶記錄。
GET /email/74657374406578616d706c652e636f6d
通過查閱歷史記錄,我發現有一個操作把電子郵件地址作為原始字串進行了接收,通過ID刪除“xyz”帳戶中的電子郵件是通過以下請求完成的。
DELETE /account/xyz/emails/[email protected]
由於我會仔細驗證進入系統的所有使用者輸入,因此理論上,此電子郵件地址不會包含任何HTTP控制字元,即使它確實存在由`http`模組進行的percent escape,也不會發生這種情況。但事實卻是,電子郵件地址包含了unicode字元。
在測試環境中,我能夠建立一個帳戶,並向其中新增以下奇怪但有效的電子郵件地址。
x@̠ňƆƆɐį1̮1č̊č̊ɆͅƆ̠įaccountįf9f9eebb05ef4b819b0467cc5ddd3b4aįsessions̠ňƆƆɐį1̮1č̊č̊.cc
這裡的非ascii字元是經過我精心選擇的,因此,當在latin1中進行小寫和編碼時,它們將為各種HTTP控制字元生成原始位元組。
> v = 'x@̠ňƆƆɐį1̮1č̊č̊ɆͅƆ̠įaccountįf9f9eebb05ef4b819b0467cc5ddd3b4aįsessions̠ňƆƆɐį1̮1č̊č̊.cc' > Buffer.from(v.toLowerCase(), "latin1").toString() 'x@ HTTP/1.1\r\n\r\nGET /account/f9f9eebb05ef4b819b0467cc5ddd3b4a/sessions HTTP/1.1\r\n\r\n.cc'
通過將此電子郵件地址新增到帳戶並刪除它,我可以使API伺服器向資料儲存區發出如下所示的HTTP請求。
DELETE /account/f9f9eebb05ef4b819b0467cc5ddd3b4a/email/x@̠ňɔɔɐį1̮1č̊č̊ɇͅɔ̠įaccountįf9f9eebb05ef4b819b0467cc5ddd3b4aįsessions̠ňɔɔɐį1̮1č̊č̊.cc
藉助上述Node.js中的漏洞,此HTTP請求將被寫入以下內容。
> console.log(Buffer.from('DELETE /account/f9f9eebb05ef4b819b0467cc5ddd3b4a/email/x@̠ňɔɔɐį1̮1č̊č̊ɇͅɔ̠įaccountįf9f9eebb05ef4b819b0467cc5ddd3b4aįsessions̠ňɔɔɐį1̮1č̊č̊.cc', 'latin1').toString()) DELETE /account/f9f9eebb05ef4b819b0467cc5ddd3b4a/email/x@ HTTP/1.1 GET /account/f9f9eebb05ef4b819b0467cc5ddd3b4a/sessions HTTP/1.1 .cc
這是一個SSRF,導致API伺服器產生一個額外的“GET”請求,而這並不是它想要的。
雖然,這個特定的“GET”請求是無害的,但它足以讓我相信這個漏洞是可利用的,比如,它可能會被用來誘導API伺服器對資料儲存API發出各種各樣的欺詐性請求,比如建立一個使用者無法控制的電子郵件地址,或重置其他使用者帳戶的密碼,或者只是在Firefox帳戶對電子郵件地址強加的255-unicode字元長度限制內進行任何操作。
幸運的是,目前還沒有任何證據表明這個漏洞被利用。
還需要注意的是,攻擊者無法利用此漏洞訪問使用者的Firefox同步資料。Firefox同步資料使用強大的客戶端加密功能,只有知道帳戶密碼的人才能訪問你的同步資料。
總結
一發現此漏洞,我就將Node.js漏洞報告給了Firefox團隊,而且在今年4月釋出的Node.js 10中,此漏洞已經被修復。
Node.js 10.0.0是自 Node.js Foundation開展以來的第七個主要版本,並將在2018年10月成為下一個LTS分支。
完整更新內容請查閱發行說明: