RunC TOCTOU逃逸CVE-2021-30465分析

雲鼎實驗室發表於2021-08-18

背景

國外安全研究員champtar[1]在日常使用中發現Kubernetes tmpfs掛載存在逃逸現象,研究後發現runC存在條件競爭漏洞,可以導致掛載逃逸[2]


關於條件競爭TOCTOU和一些linux檔案基礎知識可見這篇文章《初探檔案路徑條件競爭 - TOCTOU&CVE-2019-18276》[3]


CVE-2021-30465在Redteam的研究者視角中比較雞肋,因為需要K8S批次建立POD的許可權。但在產品安全的視角恰恰相反,針對Caas(Container as a service)類產品,使用者/租戶擁有批次建立POD許可權,利用掛載逃逸可打破租戶間隔離,同時讀取Host層面某些敏感資料,危害性極大。


 RunC簡介


為了讓容器生態更加開放,Linux基金會發起OCI(Open Container Initiative),目標是標準化容器格式和執行時[4],其中一個重要產物就是CRI(Container Runtime Interface),抽象了容器執行時介面,使得上層調控容器更加便捷。containerd和runC都是其中代表產物,從dockerd中再剝離出containerd[5],向上提供rpc介面,再透過containerd去管理runC。containerd在初期也是直接對runC進行管理,但為了解決containerd進行升級等操作時會造成不可用的問題,containerd再拆出containerd-shim,獨立對接runC。containerd從Runtime、Distribution、Bundle維度提供容器全生命週期的管理能力[6],runC專注於Runtime。


圖片 


圖片 


容器裝置掛載相關基礎知識

Namespace

Namespace是linux控制系統資源的抽象層,將不同的程式放置入不同的Namespace將獲得不同的資源視角,該項技術是容器實現的基礎[7]

linux提供8種不同的Namespace以提供不同維度的隔離能力,分別是:


1. Cgroup

2. IPC

3. Network

4. Mount

5. PID

6. Time

7. User

8. UTS


其中,Cgroup和Mount Namespace是最常接觸的,在容器掛載相關能力均透過Mount Namespace進行實現。Namespace的使用主要透過cloneunshare 兩個方法實現,其中clone建立新程式時,標誌位為CLONE_NEW*將會建立新的Namespace並將子程式放入該Namespace[8]unshare方法將呼叫程式放入不同的Namespace中[9]


Mount Namespace

Linux中有一個很核心的思想,那就是一切皆檔案。在該思想下,Linux透過掛載對不同裝置中的檔案進行管理。在Linux中,每一個空目錄/檔案都可以成為掛載點並設定相應的屬性。在Mount Namespace下,處在當前Namespace中的程式只對當前Namespace中的掛載點可見,透過


/proc/[pid]/mounts/proc/[pid]/mountinfo/proc/[pid]/mountstats


提供不同維度的資料。每個task(在Linux中,不論是程式還是執行緒,在核心的視角都是一個task)都會指向一個Namesapce(存放在tasknsproxy中)[10]


RunC TOCTOU逃逸CVE-2021-30465分析


但Mount Namespace的引入也帶來了新的問題,由於Mount Namespace中的隔離性,當使用者需要掛載一個新的磁碟使所有Namespace可見時,就需要在所有的Namespace中都進行一次掛載,很麻煩,於是2.6.15中引入了共享子樹(Shared Subrees)。透過在不同掛載點設定不同的屬性,使掛載事件在不同的維度(peer group[11])進行傳播。目前支援以下四種傳播型別,其中MS_SHAREDMS_SLAVE比較常見。


1. MS_SHARED

2. MS_PRIVATE

3. MS_SLAVE

4. MS_UNBINDABLE


MS_SLAVE傳播屬性的掛載點下,父掛載點(Master)的傳播事件可以接收,但子掛載點下(Slave)的掛載事件不再傳播,容器的Rootfs掛載即為該種型別,也就是說在容器中掛載的掛載動作是不影響宿主機的,保證了容器隔離。


圖片


漏洞分析

RunC漏洞掛載邏輯分析

checkout到修復commit(0ca91f44f1664da834bc61115a849b56d22f595f)[12]的上一個版本commitc(01a56034f5ab0c1aa314377a499fe60a9c26b36)

整體流程如下



圖片




RunC透過命令runc create + runc start 或runc run啟動一個容器,runc create主要分為兩部分,一部分是準備容器程式的啟動引數,與真正實施容器runc init程式進行互動,保證容器初始化順利進行;另外一部分是執行克隆出的runc init程式,加入各種namespace並初始化容器程式的執行環境。本文以第二部分為切入點進行分析,從

libcontainer/standard_init_linux.golinuxStandardInit.Init()開始,在其中呼叫prepareRootfs,準備初始化rootfs並進行掛載。


圖片


prepareRootfs 中,呼叫 prepareRoot 設定初始掛載點,並設定掛載標誌位為 unix.MS_SLAVE | unix.MS_REC ,其後使用 mountToRootfs 對container.json中配置的掛載進行操作。


圖片 

prepareRoot 中設定容器根目錄掛載標誌位為unix.MS_SLAVE | unix.MS_REC ,容器在初始的時候會透過映象中的容器標準包(bundle)掛載根檔案系統(BaseFS),在這裡runC預設將掛載點(Propagation Type)設定為slave。由於當前已經處於容器的mount namespace中,所以當前 為容器根路徑。rootfsParentMountPrivate 函式確保上一層的掛載點是PRIVATE ,應該是出於防止逃逸的考慮。


圖片


mountToRootfs 中,針對不同的裝置型別存在不同的處理邏輯。


圖片 

在tmpfs的處理邏輯中,configs.EXT_COPYUP預設為1。


圖片 

首先準備/tmp 目錄,在prepareTmp 函式中將這個掛載點設定為 MS_PRIVATE ,再建立runctmpdir 路徑,將目標路徑複製到 tmpDir 中,最後將dest 路徑掛載到tmpDir 中,且Propagation Type設定為MS_MOVE 。對於MS_MOVE ,官方說明[13]如下:


If mountflags contains the flag MS_MOVE (available since Linux 2.4.18), then move a subtree: source specifies an existing mount point and target specifies the new location to which that mount point is to be relocated. The move is atomic: at no point is the subtree unmounted.


當此時的dest 為一個symlink時,subtree將覆蓋已存在掛載點。所以此處存在TOCTOU(Time-of-check to time-of-use),在SecureJoin 函式執行時,dest 為正常路徑,當掛在發生時,dest 為symlink,導致逃逸發生。


結論


RunC為了防止在路徑組合中的路徑穿越漏洞,引入了filepath-securejoin[14]作為符號連結過濾函式,但r在掛載時並未校驗掛載的實際目的路徑,從而導致存在TOCTOU條件競爭漏洞。


從securejoin的Readme中也可看出這一點


圖片 

之所以能夠成功逃逸的另一原因在於tmpfs[15]中為了實現copy-up功能[16]使用MS_MOVE[17]作為掛載標誌,根據runC作者的描述只有在tmpfs情況才能夠逃逸[18]


補丁分析

在補丁中,可以看出在tmpfs的掛載邏輯中,增加了doTmpfsCopyUp 函式。


圖片 

在其中使用WithProcfd 函式防止TOCTOU漏洞的發生,所有的 securejoin.SecureJoin 移入WithProcfd進行統一處理。


圖片 

WithProcfd 中使用/proc/self/fd/ ,確保開啟的檔案是securejoin.SecureJoin 後的檔案。


圖片

POC分析

漏洞作者給出的POC中給出了一個很精妙的構造,利用了這個看似很難利用的條件競爭漏洞。


首先,建立兩個公共tmpfs的掛載,名稱為test1、test2,在容器A中,將test1掛載到/test1路徑,test2掛載到/test2路徑,同時將/test2/test2指向/ 。在容器B中,將test1掛載到/test1路徑,test2掛載到/test1/mntx路徑和/test1/zzz路徑。


在容器A啟動後,將/test1/mnt-tmpx指向rootfs路徑,且交換mnt和mnt-tmpx,且rootfs/test2/test2指向/(K8S中,同一個pod下的rootfs在一個路徑,形如/var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir )。


所以當條件競爭掛載的時候,即容器B啟動時,掛載test2,

mount('/','rootfs/test1/zzz')  ,同時MS_MOVE 標誌位將原有該掛載點的subtree移至新掛載點下,造成逃逸發生。

總結

Linux在引入symlink的時候並不存在安全風險,但隨著時代的變遷(容器的引入),symlink確實在一定程度上確實容易造成容器逃逸的發生。[19]Linux在嘗試在不同的角度去解決這個問題,但目前還沒有很完全的能夠解決此風險。這裡不禁讓人想引用tk的一句話:


安全意識要有時代背景。


作者認為伴隨容器場景愈發複雜,安全研究的逐漸深入,非Linux核心漏洞導致的容器逃逸長期來看還會有一個增長的趨勢。


參考資料

[1]  champtar

https://github.com/champtar

[2]runc mount destinations can be swapped via symlink-exchange to cause mounts outside the rootfs (CVE-2021-30465) 

https://blog.champtar.fr/runc-symlink-CVE-2021-30465/

[3] 《初探檔案路徑條件競爭 - TOCTOU&CVE-2019-18276》

http://whip1ash.cn/2021/06/16/toctou/

[4] About the Open Container Initiative

https://opencontainers.org/about/overview/

[5] What is the relationship between containerd, OCI and runc?

http://www.caict.ac.cn/kxyj/qwfb/ztbg/202010/t20201021_360375.htm

[6] Containerd Architecture

https://github.com/docker-archive/containerd/blob/master/design/architecture.md

[7] namespaces(7) — Linux manual page

https://man7.org/linux/man-pages/man7/namespaces.7.html

[8] clone(2) — Linux manual page

https://man7.org/linux/man-pages/man2/clone.2.html

[9] unshare(2) — Linux manual page

https://man7.org/linux/man-pages/man2/unshare.2.html

[10] Linux Namespace分析——mnt namespace的實現與應用

https://hustcat.github.io/namespace-implement-1/

[11]Mount namespaces and shared subtrees

https://lwn.net/Articles/689856/

[12] 0ca91f44f1664da834bc61115a849b56d22f595f

https://github.com/opencontainers/runc/commit/0ca91f44f1664da834bc61115a849b56d22f595f

[13] mount(2) — Linux manual page

https://man7.org/linux/man-pages/man2/mount.2.html

[14] filepath-securejoin

https://github.com/cyphar/filepath-securejoin

[15]  tmpfs

https://en.wikipedia.org/wiki/Tmpfs

[16] Overlay Filesystem

https://www.kernel.org/doc/html/latest/filesystems/overlayfs.html#non-directories

[17] mount(2) — Linux manual page

https://man7.org/linux/man-pages/man2/mount.2.html

[18]  rootfs: add mount destination validation

https://github.com/opencontainers/runc/commit/0ca91f44f1664da834bc61115a849b56d22f595f

[19] Re: [PATCH 2/3] namei: implement AT_THIS_ROOT chroot-like path resolution

https://lwn.net/ml/linux-kernel/CAG48ez30WJhbsro2HOc_DR7V91M+hNFzBP5ogRMZaxbAORvqzg@mail.gmail.com/


相關文章