作者:1mperio@云鼎實驗室
原文鏈接:https://mp.weixin.qq.com/s/FQdNaLduPX4jZBKluOqFOA

0x01 背景

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

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

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

0x02 RunC簡介

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

0x03 容器設備掛載相關基礎知識

Namespace

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

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并將子進程放入該Namespaceunshare 方法將調用進程放入不同的Namespace中

Mount Namespace

Linux中有一個很核心的思想,那就是一切皆文件。在該思想下,Linux通過掛載對不同設備中的文件進行管理。在Linux中,每一個空目錄/文件都可以成為掛載點并設置相應的屬性。 在Mount Namespace下,處在當前Namespace中的進程只對當前Namespace中的掛載點可見,通過 /proc/[pid]/mounts/proc/[pid]/mountinfo/proc/[pid]/mountstats 提供不同維度的數據。每個task(在Linux中,不論是進程還是線程,在內核的視角都是一個task)都會指向一個Namesapce(存放在task→nsproxy中)。

struct nsproxy {
    atomic_t count;
    struct uts_namespace *uts_ns;
    struct ipc_namespace *ipc_ns;
    struct mnt_namespace *mnt_ns;
    struct pid_namespace *pid_ns;
    struct net       *net_ns;
};

由于容器在啟動過程中,libcontainer會將一個新的Namespace并且將容器進程移入該Namespace下,所以不同的容器進程所指向的Namespace是不同的。

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

  1. MS_SHARED
  2. MS_PRIVATE
  3. MS_SLAVE
  4. MS_UNBINDABLE

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

0x04 漏洞分析

RunC漏洞掛載邏輯分析

checkout到修復commit(0ca91f44f1664da834bc61115a849b56d22f595f)的上一個版本commit(c01a56034f5ab0c1aa314377a499fe60a9c26b36)。

整體流程如下

分析

RunC通過命令 runc create + runc startrunc 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 ,官方說明如下:

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作為符號鏈接過濾函數,但r在掛載時并未校驗掛載的實際目的路徑,從而導致存在TOCTOU條件競爭漏洞。

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

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

0x05 補丁分析

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

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

WithProcfd 中使用 /proc/self/fd/ ,確保打開的文件是securejoin.SecureJoin 后的文件。

0x06 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移至新掛載點下,造成逃逸發生。

0x07 總結

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

安全意識要有時代背景。

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

0x08 Reference

1.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/

2.About the Open Container Initiative

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

3.What is the relationship between containerd, OCI and runc?

https://gitlab.cncf.ci/containerd/containerd/blob/revert-1502-test-image/docs/index.md

4.Containerd Architecture

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

5.namespaces(7) — Linux manual page

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

6.clone(2) — Linux manual page

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

7.unshare(2) — Linux manual page

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

8.Linux Namespace分析——mnt namespace的實現與應用

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

9.Mount namespaces and shared subtrees

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

10.mount(2) — Linux manual page

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

11.filepath-securejoin

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

12.tmpfs

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

13.Overlay Filesystem

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

14.mount(2) — Linux manual page

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

15.rootfs: add mount destination validation https://github.com/opencontainers/runc/commit/0ca91f44f1664da834bc61115a849b56d22f595f

16.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/


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1674/