RunC TOCTOU逃逸CVE-2021-30465分析
背景
國外安全研究員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的使用主要透過clone和unshare 兩個方法實現,其中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]。
但Mount Namespace的引入也帶來了新的問題,由於Mount Namespace中的隔離性,當使用者需要掛載一個新的磁碟使所有Namespace可見時,就需要在所有的Namespace中都進行一次掛載,很麻煩,於是2.6.15中引入了共享子樹(Shared Subrees)。透過在不同掛載點設定不同的屬性,使掛載事件在不同的維度(peer group[11])進行傳播。目前支援以下四種傳播型別,其中MS_SHARED和MS_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.go的linuxStandardInit.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/
相關文章
- 面對runc逃逸漏洞,華為雲容器為您保駕護航2019-02-14
- Golang逃逸分析2019-08-15Golang
- JVM的逃逸分析2020-08-03JVM
- Go記憶體逃逸分析2022-02-28Go記憶體
- Go記憶體管理逃逸分析2023-03-15Go記憶體
- 深入理解Java中的逃逸分析2019-03-01Java
- 從一個例子看Go的逃逸分析2022-07-04Go
- 面試官:簡單聊聊 Go 逃逸分析?2022-04-15面試Go
- Go語言之變數逃逸(Escape Analysis)分析2020-09-30Go變數
- LUA指令碼虛擬機器逃逸技術分析2020-08-19指令碼虛擬機
- 逃逸分析:分離物件、標量替換、同步鎖消除2024-01-17物件
- Go語言變數生命期和變數逃逸分析2019-05-17Go變數
- qemu逃逸系列2022-11-21
- python沙箱逃逸2020-02-16Python
- runc hang 導致 Kubernetes 節點 NotReady2022-07-04
- 模板注入&沙箱逃逸2024-05-29
- 聊聊缺陷逃逸率2024-05-17
- runc 1.0-rc6 釋出之際2019-03-01
- runc 1.0-rc7 釋出之際2019-03-31
- python 棧幀沙箱逃逸2024-08-01Python
- python棧幀沙箱逃逸2024-06-11Python
- 你為什麼不應該過度關注go語言的逃逸分析2024-10-21Go
- 面試官:Java中物件都存放在堆中嗎?你知道逃逸分析?2022-03-15面試Java物件
- go中的記憶體逃逸2023-11-02Go記憶體
- PHP反序列化字串逃逸2021-02-19PHP字串
- 如果面試官問你 JVM,額外回答逃逸分析技術會讓你加分!2021-07-16面試JVM
- 高危!!Kubernetes 新型容器逃逸漏洞預警2022-03-03
- docker逃逸漏洞復現(CVE-2019-5736)2021-10-25Docker
- 美創安全實驗室 | Docker逃逸原理2020-08-03Docker
- Swift-逃逸閉包、自動閉包2018-06-12Swift
- 兩道題淺析PHP反序列化逃逸2023-12-04PHP
- 2022 SDC 議題 | Parallels Desktop虛擬機器逃逸之旅2022-09-27Parallel虛擬機
- 《細胞發現》:分析近10萬個細胞,復旦附屬中山醫院團隊發現了肝癌免疫逃逸新機制!2023-03-29
- CVE-2021-26119 PHP Smarty 模版沙箱逃逸遠端程式碼執行漏洞2022-01-22PHP
- 利用容器逃逸實現遠端登入k8s叢集節點2021-01-21K8S
- swift tabview 帶引數請求網路。多條目展示。json解析,逃逸閉包2020-10-08SwiftViewJSON
- 換人!golang面試官:連怎麼避免記憶體逃逸都不知道?2021-02-25Golang面試記憶體
- 在補丁上戳個洞——利用已經被修復的漏洞實現IE沙箱逃逸2020-08-19