前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【云原生攻防研究】一文读懂runC近几年漏洞:统计分析与共性案例研究

【云原生攻防研究】一文读懂runC近几年漏洞:统计分析与共性案例研究

作者头像
绿盟科技研究通讯
发布2024-04-10 10:31:25
1600
发布2024-04-10 10:31:25
举报
一. runC简介

runC是一个开源项目,由Docker公司(之前称为Docker Inc.)主导开发,并在GitHub上进行维护。它是Docker自版本1.11起采用的默认容器运行时(runtime),也是其他容器编排平台(如Kubernetes)的基础组件之一。因此在容器生态系统中,runC扮演着关键的角色。runC是一个CLI工具,用于根据Open Container Initiative(OCI)规范在Linux系统上生成和运行容器。它是一个基本的容器运行时工具,负责启动和管理容器的生命周期,包括创建、运行、暂停、恢复和销毁容器。通过使用runC,开发人员和运维人员可以更加灵活地管理容器,并且可以在不同的容器平台之间实现容器的互操作性。

然而,近年来不断披露的漏洞给runC带来了严重的安全风险。根据OpenCVE网站[1]的相关统计,runC漏洞最早可以追溯到2016年,截止到今年2月,被公网披露出和runC密切相关的漏洞已有11个(经过笔者二次筛选,剔除了和runC组件无关的,但是还有部分runC漏洞被归档到了Docker)。接下来我们将首先对这些漏洞做一个统计分析,提炼这些漏洞成因的共性。其次分析这些漏洞共通的利用技巧,最后给出针对这类漏洞的处置建议以及如何借助绿盟云原生靶场更好地赋能相关领域的研究。

二. runC漏洞统计分析

如下表所示,runC披露时间主要集中在近5年;从漏洞类型上看,runC漏洞大体可分为以下两大类:权限升级和容器逃逸(本质上绕过AppArmor和SELinux限制也属于这两大类)。权限升级是指攻击者试图从当前的权限级别提升到更高的权限级别,以获取对系统资源更广泛的访问权限。容器逃逸是指攻击者试图从容器中逃脱,获取对宿主机系统的访问权限。这两种类型的漏洞对容器都危害极大。

表1 runC漏洞统计

三. runC漏洞成因分析

通常,我们都喜欢把云原生中的漏洞成因单纯的分为逻辑缺陷、权限控制不当、配置不当等等几大类,但笔者认为这样划分过于粗粒度,不利于我们去研究具体的漏洞利用,即使再碰到类似的攻击场景,可能还是不清楚该如何做。因此,本文将深入到这些runC漏洞攻击步骤、代码等细节,根据这些细节将漏洞的成因归结为以下几大类,对于不好归类的漏洞,我们也尽可能去解释清楚其利用原理:

表2 runC漏洞成因分类

如果熟悉runC漏洞的读者可能已能从上表看出,占比较大的两类漏洞(挂载卷时条件竞争和文件描述符泄露)在被披露时影响都比较大,已经有不少安全研究人员在博客、公众号对这些漏洞都进行了详细分析。

3.1

挂载卷时条件竞争

我们将CVE-2019-19921、CVE-2021-30465、CVE-2023-27561这三个漏洞成因统一归结为挂载卷时条件竞争。有意思的是在引入CVE-2021-30465漏洞patch的过程中破坏了之前已经修复好了的CVE-2019-19921漏洞,研究人员由此申请了CVE-2023-27561漏洞[13][14][15](但它们披露时间上却整整相差了两年,因此在漏洞研究中温故知新是必要的)。这三个漏洞在利用技巧上是极其相似的。下面我们将重点介绍下CVE-2019-19921、CVE-2021-30465这两个漏洞的利用技巧,然后基于他们的利用技巧提炼这类漏洞成因的共性。

CVE-2019-19921:控制共享卷的两个容器的攻击者可以通过向 rootfs 添加指向卷上目录的符号链接,在容器初始化期间竞争卷安装。

具体来说,攻击者可能的步骤如下[10][11]:

1. 攻击者准备了两个容器镜像A和B,并设置它们共享同一个命名卷(named volume)。

2. 在容器A的rootfs中创建了一个符号链接 /proc -> /evil/level1,同时指定了命名卷挂载到了路径 /evil。

3. 容器B在容器A之前启动,并且在共享的命名卷中不断地交换 /evil/level1 和 /evil/level1~。

4. 当容器A启动时,它试图将procfs挂载到路径 /evil/level1~/level2,但由于容器B不断地交换 /evil/level1 和 /evil/level1~,容器A实际上会将procfs挂载到了 /evil/level1/level2/sys 而不是预期的路径。

5. 这导致容器A在重新挂载 /proc/sys 时,将它挂载到了 /evil/level1/level2/sys,从而使得容器A可以对主机上的敏感路径(如 /proc/sys/kernel/core_pattern)进行写入操作。

通过利用这个竞态条件,攻击者可以在容器A中成功修改主机上的敏感路径,并最终导致容器逃逸,获得对主机的更高权限。

CVE-2021-30465:runC 使用“filepath-securejoin”库中的SecureJoinVFS函数来解析传进来的文件路径是否合法。runC在调用 SecureJoinVFS 函数解析之后会将源目录挂载到通过校验的目标目录中。

具体来说,攻击者可能的步骤如下:

1. (正常容器)调用 SecureJoinVFS 函数解析传入的文件路径是否合法。

2. (恶意容器)如1解析合法后,立马用符号链接替换检查的目标文件时,通过精心构造符号链接可以将主机文件目录挂载到(正常)容器中。

因为该漏洞是利用竞争条件来进行利用的,因此有很大概率会失败。关于该漏洞更多的利用细节可以见[4][5]。

利用共性分析:以上内容我们可以看到这类漏洞利用具有以下四点共性

1. 竞争条件的利用:两个漏洞都利用了竞争条件,在容器初始化过程中通过操作文件系统或路径来达到特权提升或容器逃逸的目的。

2. 共享资源的利用:攻击者利用了容器间共享的资源,如命名卷或文件系统,通过在共享资源上进行修改或替换,来影响其他容器或主机系统。

3. 路径和符号链接的利用: 漏洞利用过程中都涉及到了路径和符号链接的操作。攻击者通过修改符号链接或替换检查目标文件的符号链接,来影响容器或主机系统的行为。

4. 主机敏感路径的利用:两个漏洞中,攻击者均利用了对主机敏感路径的修改或挂载,例如修改了主机上的 /proc/sys/kernel/core_pattern 路径,从而实现了容器逃逸或特权提升。

3.2

文件描述符泄露

CVE-2019-5736、CVE-2024-21626这两个漏洞的成因都与文件描述符泄露有关。

CVE-2019-5736:利用该漏洞的一个核心要素就是借助/proc/[PID]/exe符号链接。/proc/[PID]/exe的特殊之处在于,我们打开这个文件后在权限检查通过的情况下,内核将直接返回一个指向该文件的描述符,而非按照传统打开方式去做路径解析和文件查找。这样一来,它实际上绕过了mnt命名空间及chroot对一个进程能够访问到的文件路径的限制。

因此在runC exec加入到容器的命名空间之后,容器内进程已经能够通过内部/proc观察到它,此时如果打开/proc/[runc-PID]/exe并写入一些内容,就能够实现将宿主机上的runC二进制程序覆盖掉。这样一来,下一次用户调用runC去执行命令时,实际执行的将是攻击者放置的指令。具体细节可以绿盟科技研究通讯相关公众号文章《容器逃逸成真:从CTF解题到CVE-2019-5736漏洞挖掘分析》[6]。

CVE-2024-21626:runC run 或者 runC exec 的过程中存在没有及时关闭的fd,从而导致文件描述符泄漏在容器环境中,泄漏的文件描述符指向 /sys/fs/cgroup,攻击者可以利用该文件描述符的/proc/self/fd/符号链接访问宿主机文件系统。具体细节可以见绿盟科技研究通讯公众号文章《【云原生攻防研究】— runC再曝容器逃逸漏洞(CVE-2024-21626)》[2]。

利用共性分析:以上内容我们可以提炼出以下共性

1. 符号链接的利用:这两个漏洞都利用了符号链接,其中CVE-2019-5736利用了/proc/[PID]/exe符号链接,而CVE-2024-21626利用了/sys/fs/cgroup目录下的符号链接。攻击者通过操纵符号链接,绕过了权限检查和文件路径限制,进而实现对宿主机文件系统的访问或覆盖重要文件的目的。

2. 文件描述符泄漏的利用:其中CVE-2019-5736是借助/proc/[PID]/exe的特殊功能拿到文件描述符。而CVE-2024-21626则是由于runC启动过程中操作 Cgroup的代码忘记使用 O_CLOEXEC flag(执行exec前自动关闭文件描述符),导致文件描述符泄漏到容器环境中。

3.3

挂载错误目标

CVE-2019-16884:libcontainer/rootfs_linux.go 错误地检查挂载目标,因此恶意 Docker 映像可以挂载在 /proc 目录上,从而绕过 AppArmor 限制(根据漏洞发现者的分析,该漏洞也影响SELinux)。具体来说,漏洞成因位于rootfs_linux.go文件中的checkMountDestination函数,该函数中会对需要挂载的目标路径进行合法判断(runc在挂载时会做黑名单校验,不允许挂载的目的路径为/proc)。该函数中对invalidDestinations的判断存在严重的逻辑问题,如图所示的这段代码,是完全放通目的路径为/proc的情况的。

图1 checkMountDestination函数[17]

项目的测试代码同样存在问题,错把==写错为!=,最终导致及时在测试阶段也没排除bug。整个问题函数的调用链如下:libcontainer.Init() -> prepareRootfs() -> mountToRootfs() -> checkMountDestination()。更多的细节可以参考[16][18]。

CVE-2023-28642:容器内的 /proc 目录被符号链接到具有特定挂载配置的路径时,可能绕过 AppArmor和SELinux 的限制。在容器的初始化阶段,应用AppArmor策略就是将exec {ProfileName}写入/proc/self/attr/exec中。

利用共性分析:以上内容我们可以提炼出以下共性

1. AppArmor和SELinux开启方式的利用:容器中AppArmor和SELinux这类LSM机制的开启都是向procfs写入label,攻击者只需要阻止写入的过程即可避免LSM的开启。类似的攻击手法可能适用于其他安全机制。

2. Procfs的利用:procfs是一个伪文件系统,实际是内核虚拟文件系统。如果攻击者可以控制/proc,则实际的AppArmor和SELinux策略就不会被内核应用。因此runC在挂载时会做黑名单校验,不允许挂载/proc,攻击者的目标就是绕过这个校验。

3.4

其它

CVE-2016-3697:runC在处理Linux用户时错误地将一个数字型的UID(用户ID)视为潜在的用户名(用户名字符串),而非将其解析为数字形式的UID。这将导致一个问题:如果容器中的密码文件中存在一个数字用户名(比如1000),runC就会错误地将它解释为用户名,而不是UID,从而可能导致了提权问题[9]。

如在容器的**/etc/passwd**文件中,存在一个名为1000的用户条目,内容如下所示:

1000:x:1000:1000:user3:/home/user3:/bin/bash

这个条目中的用户名是1000,UID也是1000。然而,由于runC在处理用户时的逻辑缺陷,它错误地将1000解释为一个潜在的用户名,而不是一个数字形式的UID。因此,runC可能会错误地将这个条目解释为一个名为1000的用户,而不是用户ID为1000的用户。

这可能导致潜在的提权问题。例如,如果在容器内的进程以名为1000的用户身份运行,并且容器内的某些操作依赖于用户ID为1000的权限,那么将会导致该进程越权。

CVE-2021-43784:该漏洞涉及将netlink用作内部序列化容器配置的系统。这个漏洞源于对字节数组属性类型的16位长度字段处理的疏忽。具体而言,编码器没有考虑到长度字段可能发生整数溢出的情况,允许足够大的恶意字节数组属性导致长度溢出。因此,属性内容可以被解析为用于容器配置的netlink消息[8]。

利用该漏洞需攻击者对容器配置有一定控制权。通过添加自己的netlink负载,攻击者可能绕过容器的命名空间限制,有效地禁用所有命名空间。

CVE-2022-29162:在使用 runC exec --cap 命令执行进程时,这些进程会具有非空的可继承 Linux 进程能力(Linux process capabilities)。这导致了一个不符合安全预期的 Linux 环境,使得具有可继承文件能力(inheritable file capabilities)的程序可以在执行 execve(2) 系统调用时将这些能力提升到允许的集合中。

为了更清晰的理解该漏洞利用,笔者举例说明:

假设我们有一个名为 privileged_program 的程序,它具有一个特殊的文件能力(file capability)叫做 CAP_SYS_ADMIN,这个能力允许程序执行系统管理任务,而无需完整的 root 权限。现在,假设在一个普通的 Linux 环境中,我们通过 runC exec --cap 命令执行了这个程序,但由于 runC 存在漏洞,它导致程序在执行过程中继承了非空的进程能力。这种情况下,即使普通的 Linux 用户也可以通过 runC exec --cap 命令执行 privileged_program,并继承了非空的进程能力。而这个进程能力可能会被 privileged_program 利用,将自身的文件能力提升到允许的集合中,包括 CAP_SYS_ADMIN。

因此,尽管普通用户在传统的 Linux 环境中无法执行需要 CAP_SYS_ADMIN 权限的任务,但通过漏洞利用,他们可以在容器内执行 privileged_program,并最终获取到了执行系统管理任务所需的特权,这违反了Linux中预期的安全行为,构成了一个安全隐患。

CVE-2023-25809:这个漏洞影响了rootless模式下的runC,使得容器可以在一些特定条件下获得对主机上的 /sys/fs/cgroup 目录的写权限。

具体来说,影响版本的runC在以下两种情况下可能会将 /sys/fs/cgroup 目录设为可写[12]:

1. 当runC在用户命名空间内执行,并且config.json配置文件中未指定cgroup命名空间为未共享状态时,runC可能会错误地将 /sys/fs/cgroup 目录设置为可写,导致容器可以修改主机上用户拥有的cgroup层次结构,例如 /sys/fs/cgroup/user.slice/...。

2. 当runC在用户命名空间外执行,并且 /sys 目录以 rbind, ro 的方式挂载时。这种情况下,尽管较为罕见,但同样存在安全风险,因为容器可能会利用此配置来获得对主机上 /sys/fs/cgroup 目录的写权限。

四. runC漏洞处置建议

以上漏洞案例共性分析中,我们了解到虽然runC漏洞的披露时间上可能前后不同,但它们在利用技巧及手法上是具有相似性的,更有甚者,有些相似漏洞的成因居然是修复同类型漏洞引入的。

对此,我们在这里给出一些处置建议和防护措施:

1) 及时更新和升级:确保系统中运行的runC版本是最新的,及时应用厂商发布的漏洞修复补丁和安全更新。

2) 限制容器权限:采取适当的措施限制容器的权限,例如使用适当的seccomp配置、AppArmor或SELinux策略,以及限制容器的资源访问权限等。

3) 加强输入验证:对于用户输入和容器配置等数据,加强输入验证,确保输入数据的完整性和合法性,防止恶意输入导致的安全问题。

4) 最小权限原则:在配置容器时,遵循最小权限原则,仅授予容器所需的最小权限,减少容器对系统资源的访问权限,从而降低潜在攻击面。

五. 绿盟云原生靶场助力漏洞研究

行文至此,相信读者应该对runC漏洞成因有了一个较为清晰画像,对漏洞研究者而言,光读是远远不够的(纸上得来终觉浅,绝知此事要躬行)。尤其对于初学者而言,无法避免去搭建漏洞环境,编写PoC、Exp,一步步去分析利用过程,这样才能对原理有更深刻的理解。

对此,基于星云实验室在云原生攻防上的多年积累,我们提供了云原生攻防靶场Metarget [7],该项目已在Github上开源,Metarget是一个脆弱基础设施自动化构建框架,主要用于快速、自动化搭建从简单到复杂的脆弱云原生靶机环境。可以有效帮助安全研究人员进行漏洞学习、调试,特定PoC、Exp的编写和测试;同时提升红蓝对抗、渗透测试人员的云原生安全攻防实战技能。

除此之外,云原生安全产品开发人员也可以借助Metarget项目部署脆弱环境,测试云原生防御系统的威胁检测能力和响应能力。目前,Metarget已经覆盖了我们上述介绍的绝大多数runC漏洞,绿盟的云原生容器安全产品CNSP也提供相应runC的检测规则,欢迎各位读者试用。

本文参与?腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2024-04-08,如有侵权请联系?cloudcommunity@tencent.com 删除

本文分享自 绿盟科技研究通讯 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与?腾讯云自媒体分享计划? ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
http://www.vxiaotou.com