Web安全漏洞之SSRF
什麼是 SSRF
大家使用的服務中或多或少是不是都有以下的功能:
- 通過 URL 地址分享內容
- 通過 URL 地址把原地址的網頁內容調優使其適合手機螢幕瀏覽,即所謂的轉碼功能
- 通過 URL 地址翻譯對應文字的內容,即類似 Google 的翻譯網頁功能
- 通過 URL 地址載入或下載圖片,即類似圖片抓取功能
- 以及圖片、文章抓取收藏功能
簡單的來說就是通過 URL 抓取其它伺服器上資料然後做對應的操作的功能。以 ThinkJS 程式碼為例,我們的實現方法大概如下:
const request = require('request-promise-native'); module.exports = class extends think.Controller { async indexAction() { const { url } = this.get(); const ret = await request.get(url); // 這裡是處理抓取資料的邏輯 // ... this.ctx.body = ret; } }
本來是個不錯的功能,但是當用戶輸入一個伺服器可訪問的內網地址,這個情況下它就會把內網的內容抓取出來展現給外網的使用者。大多數公司會在內網中放置一些與公司相關的資料和關鍵資料,如果應用程式對使用者提供的URL和遠端伺服器返回的資訊沒有進行合適的驗證和過濾,就可能存在這種服務端請求偽造的缺陷,即 Server-Side Request Forgery,簡稱 SSRF。
SSRF 的危害
簡單來說如果你的這個功能存在 SSRF 漏洞的話,相當於在攻擊者和內網之間牽了根線,透過該通能攻擊者可以間接訪問到內網。攻擊者可以利用 SSRF 實現的攻擊主要有 5 種:
- 可以對外網、伺服器所在內網、本地進行埠掃描,獲取一些服務的 Banner 資訊
- 攻擊執行在內網或本地的應用程式(比如溢位)
- 對內網 Web 應用進行指紋識別,通過訪問預設檔案實現
- 攻擊內外網的 Web 應用,主要是使用 GET 引數就可以實現的攻擊
- 利用 file 協議讀取本地檔案
其中最後一條的實現方式是使用者輸入 file://
本地檔案協議地址,如果不做判斷,程式很可能就會把本地檔案讀取出來返回給使用者,例如 file:///etc/password
伺服器系統密碼。
防禦方法
首先我們需要 禁用掉不需要的協議 ,僅允許 HTTP(s) 請求,防止最後一條使用 file://
等其它協議引起的問題,然後我們需要對 輸出內容進行判斷 ,例如我應該輸出一張圖片,如果抓取返回回來的是一段文字我們就不應該返回。以及如果抓取遠端地址導致報錯返回的情況,我們需要統一處理返回給使用者的內容,而不是直接將遠端伺服器的內容返回給使用者,這樣讓攻擊者瞭解到了更多遠端伺服器的資訊。
除了輸出內容的處理,我們還要對 輸入地址進行限制 ,過濾內網 IP,限制訪問內網行為。以之前的示例程式碼為例,正常我們會增加如下處理:
const ip = require('ip'); const dns = require('dns'); const { parse } = require('url'); const lookupAsync = think.promisify(dns.lookup, dns); module.exports = class extends think.Logic { async indexAction() { const { url } = this.get(); const { protocol, hostname } = parse(url); // 判斷協議 if( !/https?:/i.test(protocol) ) { return this.fail(); } // 判斷內網IP cost host = await lookupAsync(hostname); if( ip.isPrivate(host) ) { return this.fail(); } } }
短連結繞過
大部分情況下這樣處理是沒有問題的,不過攻擊者可不是一般人。這裡存在一個兩個可以繞過的方式,首先是短連結,短連結是先到短連結服務的地址之後再302跳轉到真實伺服器上,如果攻擊者對內網地址進行短鏈處理之後以上程式碼會判斷短鏈服務的 IP 為合法 IP 而通過校驗。
針對這種繞過方式,我們有兩種方法來阻止:
- 直接根據請求返回的響應頭中的 HOST 來做內網 IP 判斷
- 由於跳轉後的地址也還是需要 DNS 解析的,所以只要在每次域名請求 DNS 解析處都做內網 IP 判斷的邏輯即可
DNS 重新繫結繞過
另外一種繞過方式是利用 DNS 重繫結攻擊。
DNS如何重新繫結的工作
攻擊者註冊一個域名(如attacker.com),並在攻擊者控制下將其代理給DNS伺服器。 伺服器配置為很短響應時間的TTL記錄,防止響應被快取。 當受害者瀏覽到惡意域時,攻擊者的DNS伺服器首先用託管惡意客戶端程式碼的伺服器的IP地址作出響應。 例如,他們可以將受害者的瀏覽器指向包含旨在在受害者計算機上執行的惡意JavaScript或Flash指令碼的網站。
惡意客戶端程式碼會對原始域名(例如attacker.com)進行額外訪問。 這些都是由同源政策所允許的。 但是,當受害者的瀏覽器執行該指令碼時,它會為該域建立一個新的DNS請求,並且攻擊者會使用新的IP地址進行回覆。 例如,他們可以使用內部IP地址或網際網路上某個目標的IP地址進行回覆。
via: 《DNS 重新繫結攻擊》
簡單來說就是利用 DNS 伺服器來使得每次解析返回不同的 IP,當在校驗 IP 的時候 DNS 解析返回合法的值,等後續重新請求內容的時候 DNS 解析返回內網 IP。這種利用了多次 DNS 解析的攻擊方式就是 DNS 重新繫結攻擊。
由於 DNS 重新繫結攻擊是利用了多次解析,所以我們最好將校驗和抓取兩次 DNS 解析合併成一次,這裡我們也有兩種方法來阻止:
- 將第一次 DNS 解析得到的 IP 直接用於第二次請求的 DNS 解析,去除第二次解析的問題
- 在抓取請求發起的時候直接判斷解析的 IP,如果不符合的話直接拒絕連線
針對以上解決方法,有開發者直接封裝了 ssrf-agent 模組,使用的時候只要將其傳入即可實現一次解析,多次判斷的功能,下面是簡單的使用示例:
const ssrfAgent = require('ssrf-agent'); const request = require('request-promise-native'); module.exports = class extends think.Controller { async indexAction() { const { url } = this.get(); try { const ret = await request(url, {agent: ssrfAgent(url)}); this.ctx.body = ret; } catch(e) { return this.fail(); } } }
後記
SSRF 可以說是經久不衰的漏洞攻擊了,早些年百度、人人、360搜尋等都有過相應的案例。一般以下場景會可能存在 SSRF 問題,我們需要多加註意:
- 能夠對外發起網路請求的地方,就可能存在 SSRF 漏洞
- 從遠端伺服器請求資源(Upload from URL,Import & Export RSS Feed)
- 資料庫內建功能(Oracle、MongoDB、MSSQL、Postgres、CouchDB)
- Webmail 收取其他郵箱郵件(POP3、IMAP、SMTP)
- 檔案處理、編碼處理、屬性資訊處理(ffmpeg、ImageMagic、DOCX、PDF、XML)