影響大量雲服務廠商的嚴重漏洞:runC容器逃逸漏洞分析(CVE-2019-5736)
一、概述
以下漏洞研究的靈感來源於35C3 CTF的namespaces 任務,由_tsuro建立。在進行這一挑戰的過程中,我們發現,從安全形度來看,要建立基於名稱空間的沙箱並通過外部程序加入是一項非常具有挑戰性的任務。我們在CTF比賽結束後,發現Docker具有“docker exec”功能(實際上是由opencontainers的runc實現的)具有類似的模型,於是我們決定挑戰這種漏洞實現。
二、目標及結果
我們的目標是在預設配置或加固後配置下的Docker容器內部攻陷宿主機環境(例如:獲得有限的功能和系統呼叫可用性)。我們考慮了以下兩個攻擊維度:
1、惡意Docker映象;
2、容器內的惡意程序(例如:攻陷以root身份執行的Docker化服務)。
最終的結果,我們已經在宿主機上實現了完整意義的程式碼執行,具有所有功能(獲得管理員“root”訪問許可權),該過程可以由以下任一方式觸發:
1、在被攻陷的Docker容器上,從宿主機執行“docker exec”;
2、啟動惡意Docker映象。
該漏洞被分配CVE編號CVE-2019-5736,並在這裡正式公佈:https://seclists.org/oss-sec/2019/q1/119
三、預設Docker安全設定
儘管Docker並沒有作為沙盒軟體發行,但它的預設設定會保護宿主機資源不被容器內的程序訪問。儘管Docker容器中的初始程序是以root身份執行的,但實際上它具有非常有限的許可權,這是通過幾種不同的機制來實現的,這篇文章 對其進行了詳細描述。
1、Linux功能
預設情況下,Docker容器具有非常有限的功能集,這使得容器root使用者實際上是一個非特權使用者。
2、seccomp
這一機制能夠阻止容器的程序執行系統呼叫的子集或者過濾其引數,從而限制其對宿主機環境的影響。
這一機制允許限制容器化程序對宿主機檔案系統的訪問,並限制跨主機、跨容器邊界程序之間的可見性。
4、cgroups
控制組(cgroups)機制允許限制和管理一組程序的各種型別的資源(例如:RAM、CPU等)。
上面的這些機制都可以被禁用(例如:通過使用—privileged命令列選項),或者明確指定任何一組系統呼叫/功能/共享的名稱空間。如果禁用了這些加固機制,就可以輕鬆實現容器逃逸。但我們此次嘗試將在預設安全配置的Docker容器中進行。
四、失敗的漏洞利用方案
在最終找到這一漏洞之前,我們嘗試過許多其他想法,其中大部分思路都是通過有限的功能或者seccomp過濾器來實現的。
由於這一研究過程是在35C3 CTF之後的後續工作,因此我們首先要調查在現有名稱空間中啟動新程序時會發生什麼(也就是docker exec)。我們的目標是,檢查是否可以通過新加入的程序獲取它們,以訪問某些宿主機資源。想象以下場景,其中的某個程序:
1、加入使用者和PID名稱空間;
2、forks(實際加入PID名稱空間);
3、加入其餘的名稱空間(mount、net等)。
如果我們可以在看到它時(也就是該程序加入PID名稱空間時),立即對該程序進行ptrace,就可以阻止它加入其它的名稱空間,但反過來又可以啟用。
通過容器初始化程序執行為共享的使用者名稱空間,可以繞過不必須的ptrace功能(會產生新使用者名稱空間中的完整功能集)。然後,“docker exec”將會加入到新的名稱空間(通過/proc/pid/ns獲得),我們可以在其中進行ptrace(但仍然具有seccomp的限制)。事實證明,runc在完成之後,加入了所有必須的名稱空間,並在完成後只進行fork,這樣就成功阻止了這一攻擊向量。此外,預設的Docker配置還會禁用容器內所有與名稱空間相關的系統呼叫(例如:setns、unshare等)。
接下來,我們專注於proc檔案系統(proc(5),更多資訊請參考:http://man7.org/linux/man-pages/man5/proc.5.html),因為它非常特殊,並且通常可以跨越名稱空間的邊界。其中,有如下幾個有趣的條目:
1、/proc/pid/mem:由於目標程序需要在與惡意程序相同的PID名稱空間中,因此這不能給我們帶來太多幫助。同樣,ptrace(2)也是如此。
2、/proc/pid/cwd和/proc/pid/root:在程序完全加入容器之前(加入名稱空間之後,更新其根目錄chroot和cwd cwdir之前),這些條目都指向宿主機檔案系統,可能允許我們對其進行訪問。但由於runc程序不可轉儲(http://man7.org/linux/man-pages/man2/ptrace.2.html),因此我們無法利用。
3、/proc/pid/exe:本身沒有任何用處(與第2條相同的原因),但我們已經找到了解決方法,並在最終的漏洞利用方案中利用了它們(詳見下文)。
4、/proc/pid/fd/:某些檔案描述符可能從父級名稱空間(特別是mount名稱空間)中洩露,或者我們可能會在runc中干擾父子間(實際上是孫子間)的通訊。我們無法利用這裡,因為同步過程已經使用過了本地套接字,不能再重複使用。
5、/proc/pid/map_files/:這是一個非常有趣的向量。在runc執行目標二進位制檔案之前(在程序對我們可見,即程序加入PID名稱空間之後),所有條目都引用來自宿主機檔案系統的二進位制檔案。不幸的是,我們發現,如果沒有SYS_ADMIN功能(原始碼:https://elixir.bootlin.com/linux/v4.20.7/source/fs/proc/base.c#L2037),我們就無法利用這些連結,即使在同一個程序中也是如此。
附註1:在執行以下命令時
/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 /bin/ls -al /proc/self/exe
“/proc/self/exe”實際指向了“ld-linux-x86-64.so.2”(不是“/bin/ls”,正如人們所想的那樣)。
攻擊思路是強制“docker exec”使用來自宿主機的動態載入器來執行容器內的二進位制檔案/evil_binary。具體而言,是使用文字檔案第一行的內容#!/proc/self/map_files/address-in-memory-of-ld.so替換原始目標到exec(例如:/bin/bash)。
隨後,/evil_binary就可以覆蓋/proc/self/exe,從而覆蓋宿主機的ld.so。由於上述所提到的SYS_ADMIN功能要求,因此這一方法不能成功。
附註2:在嘗試上面的內容時,我們在核心中發現了一個死鎖。當一個常規程序試圖執行“/proc/self/map_files/any-existing-entry”時,將會產生死鎖。從任何其他程序開啟“/proc/that-process-pid/maps”時也會發生掛起,可能也產生了一些鎖定。
五、成功的漏洞利用方案
最後,我們成功的漏洞利用方案涉及到一種非常類似於上述思路的方法,利用到/proc/self/map_files。我們執行/proc/self/exe,這是主機的docker-runc二進位制檔案,同時可以向其中注入一些程式碼。具體而言,我們首先更改某些共享庫(例如:libc.so),從而執行我們的程式碼(例如:在libc_start_main或全域性建構函式中)。這使我們能夠覆蓋/proc/self/exe二進位制檔案,該檔案是來自宿主機的docker-runc二進位制檔案,反過來也會在下次執行docker-runc時為我們提供對宿主機的完全訪問許可權。
詳細攻擊說明如下。
首先,製作惡意映象,或攻陷正在執行的容器:
1、將入口點二進位制檔案(或任何可能被使用者作為入口點或作為docker exec一部分被執行時覆蓋的二進位制檔案)稱為/proc/self/exe的符號連結;
2、將docker-runc使用的任何動態庫替換為具有附加全域性建構函式的自定義.so。該函式將開啟/proc/self/exe(指向主機docker-run)進行讀取(無法開啟它進行寫入操作,因為二進位制檔案正在執行中,請參考open(2)中的ETXTBSY)。然後,這個函式執行另一個開啟的二進位制檔案/proc/self/fd/3(在execve之前開啟的docker-runc的檔案描述符),現在就要進行寫入了。這一過程能成功執行,因為docker-runc在此時不會再被執行。然後,程式碼可以使用任意內容來覆蓋宿主機的docker-runc。我們選擇了一個假的docker-runc,以及額外一個執行任意程式碼的全域性建構函式。
因此,當主機使用者在受感染的容器上執行受感染的映象或“docker exec”時:
1、已經符號連結到/proc/self/exe(在宿主機檔案系統上指向docker-runc)的入口點/exec二進位制檔案開始在容器內執行。這一過程會導致程序可以轉儲,因為execve設定了可以轉儲的標誌。需要明確的是:這會導致原始docker-runc程序重新執行在容器內執行的新docker-runc(但使用的是宿主機二進位制檔案)。
2、當docker-runc第二次開始執行時,它將從容器(而不是宿主機)下載.so檔案。而我們已經能夠控制這些動態庫的內容。
3、執行惡意全域性建構函式。它將開啟/proc/self/exe進行讀取(例如檔案描述符3),以及execve()一些攻擊者控制的二進位制檔案(例如:/evil)。
4、/evil將覆蓋宿主機檔案系統上的docker-runc(通過重新開啟fd3,這次具有寫訪問許可權)和具有後門或惡意的docker-runc(例如:使用額外的全域性建構函式)。
5、當任何容器啟動,或其他exec完成時,攻擊者構造的docker-runc將以root身份執行,並且具有宿主機檔案系統的全部功能(該二進位制檔案負責刪除許可權並進入名稱空間,因此其最初具有完全許可權)。
請注意,這一攻擊過程僅濫用了runc(opencontainers)的行為,因此它也同樣適用於Kubernetes,無論是否使用docker或cri-o。
該攻擊將對AWS和GCP雲服務產生嚴重影響,有關更多資訊可以在安全公告中檢視。
六、負責任的漏洞披露過程
我們在發現這一漏洞的當天,向[email protected]報告了漏洞,其中包含詳細的攻擊描述和概念驗證。轉天,Docker安全團隊將我們的電子郵件轉發至[email protected]。在此過程中,我們積極參與有關漏洞修復的討論,與Docker和OpenContainers安全團隊進行了密切的溝通。
七、失敗的runc修復方案
方案1:開啟目標二進位制檔案,將fstat(2)中inode資訊與/proc/self/exe進行比較,如果匹配則退出,否則執行目標二進位制檔案fd。
這將會檢測目標二進位制檔案是否是/proc/self/exe的符號連結。為什麼是execveat呢?因為我們想要避免競態條件(Race Condition)的出現,如果在exec之間進行比較,其他一些程序會將目標二進位制檔案替換為/proc/self/exe連結。
失敗原因:當攻擊者沒有使用符號連結時,可以繞過這一過程。攻擊者可以使用帶有指向“/proc/self/exe”的動態載入程式的二進位制檔案,將“#!/proc/self/exe”作為第一行,或者只是作為一個elf檔案。
方案2:使用靜態二進位制檔案,啟動容器內的程序。
這樣做的想法是通過容器內的惡意.so檔案來避免程式碼執行的可能性(靜態二進位制檔案就意味著沒有載入.so檔案)。
失敗原因:該漏洞利用實際上不需要替換.so檔案。在重新執行/proc/self/exe(docker-runc)之後,另一個程序可以開啟/proc/<pid-of-docker-runc>/exe,這是可能的,因為execve上設定了“dumpable”(可轉儲)標誌。這一過程有些難以實際應用,因為它需要在重新執行完成和runc程序退出之間進行競態(由於沒有給出引數)。實際上,競態的視窗期非常大,我們能夠為這種情況開發出100%成功的漏洞利用方案。然而,這種方法確實可以消除其中的一個攻擊向量,也就是執行惡意映象。
八、最終runc修復方案
最後,應用以下修復程式來緩解漏洞:
1、建立一個memfd(僅存在於記憶體中的特殊檔案)。
2、將原始runc二進位制檔案複製到這一fd中。
3、在進入名稱空間之前,從這個fd重新執行runc。
這一修復程式能夠保證,如果攻擊者覆蓋了/proc/self/exe指向的二進位制檔案,將不會對宿主機造成任何損害,因為這是宿主機二進位制檔案的副本,完全儲存在記憶體中(tmpfs)。
九、緩解方案
如果當前使用未修復的runc,可以採用如下的緩解方案:
1、使用啟用SELinux的Docker容器(–selinux-enabled),從而防止容器內的程序覆蓋宿主機docker-runc二進位制檔案。
2、在宿主機上使用只讀檔案系統,至少使用只讀方式來儲存docker-runc二進位制檔案。
3、在容器內,使用低許可權使用者,或者使用對映到該使用者的uid 0的新使用者名稱空間。並且,該使用者不應對宿主機上runc二進位制檔案具有寫訪問許可權。
十、時間線
2019年1月1日 發現漏洞並編寫PoC;
2019年1月1日 向[email protected]報告漏洞;
2019年1月2日 由Docker安全團隊將漏洞報告轉交給[email protected];
2019年1月5日 進行漏洞利用思路的調整;
2019年2月11日 可以公佈CVE-2019-5736詳情;
2019年2月13日 釋出本文章。
十一、官方修復方案(譯者加)
截至本文翻譯完成時,runc、Docker、Kubernetes、亞馬遜雲AWS均已釋出官方修復方案,具體如下。
1、runc官方:https://github.com/opencontainers/runc/commit/0a8e4117e7f715d5fbeef398405813ce8e88558b
2、Docker :已在18.09.2 版本中修復這一漏洞。
https://github.com/docker/docker-ce/releases/tag/v18.09.23、Kubernetes:修復方案請參考https://kubernetes.io/blog/2019/02/11/runc-and-cve-2019-5736 。
4、AWS平臺:修復方案請參考https://aws.amazon.com/cn/security/security-bulletins/AWS-2019-002/ 。