原文出自:信息安全研究与教育,伦敦大学学院(UCL)
作者官网:https://www.benthamsgaze.org/
原文地址:https://www.benthamsgaze.org/2022/08/22/vulnerability-in-linux-containers-investigation-and-mitigation/
翻译本文已获得作者的许可,感谢作者对知识的热爱与研究以及开放、互助的精神
涉及名词概念(为行文简洁可能会部分会采取简称)
negative group:负组
primary group:主要组
supplementary groups:补充组
set-group programs:设置组程序(亦有人译做集合组,笔者认为设置组更为贴切一些)
set-user programs:设置用户程序
Open Container Initiative :开放容器计划
在操作系统中,访问控制机制被用来限制哪些程序可以打开哪些文件,其存在的时间几乎与计算机本身存在的时间一样长。即便是现在,访问控制仍然被广泛使用,与加密保护文件相比,访问控制更加灵活和高效。虽然访问控制机制的历史非常悠久,但其仍在不断创新,尤其是现在在容器中,如Docker和Kubernetes以及云提供商提供的类似技术。就目前而言,服务大多不是只在一台计算机上运行大量软件,而是拆分为在容器中运行的微服务。每个容器都与同一台计算机上的其他容器隔离,就好像它有自己的计算机和操作系统一样,并且被阻止读取其他容器中的文件。*但是,实际情况是只有一个操作系统,容器运行时会给人一种创建并存在多个操作系统的错觉。在容器运行时,其内部也应设置相应的访问控制机制(后文均简写为访问控制),因为并非容器内运行的每个程序都被允许能够访问每个文件。也可以向多个容器授予对同一个目录的访问权限,并使用访问控制来限制每个容器可以对目录内容执行的操作。如果访问控制没有正常工作,那么攻击者可能会读取或修改他们不应该读取或修改的文件。不幸的是,确实存在这样的漏洞。坏消息是,它源于规范中的遗漏,所有主流容器运行时的都会遵循该规范,因此无论您使用哪个容器工作(例如runc,crun,Kata Containers),也无论您直接使用容器(例如通过Docker或podman)还是间接使用容器(例如通过Kubernetes)),其都有可能存在这样的漏洞。好消息是,该漏洞影响了 Linux 访问控制权限中未广泛使用的功能 - 负组权限(negative group)。但是,如果您的系统确实依赖于此功能,则漏洞可能很严重。请继续阅读,您将获得关于该漏洞存在的原因以及可以采取哪些措施来缓解问题的更为详细信息。
Linux 权限简介
在Linux当中,存在用户帐户,且每个用户也是一个组的成员。每个对象(文件、目录、设备等)都有一个与其关联的所有者和所属组。该对象还具有一组与以下三个类相关联的权限:所有者、所属组和其他。这些权限告诉操作系统用户是否被允许从该对象读取内容 (r)、写入内容 (w) 以及执行该对象 (x)。如果某用户是该对象的所有者,则使用所有者类权限,如果某用户是该对象所属组的成员,则使用组类权限,否则使用其他类权限。
例如,包含公司财务数据库的文件可能由首席财务官 (CFO) 拥有,并具有所有者类权限“r+w”。它可以将该文件的组设置为“审计员”,组类权限仅为“r”,而其他类权限设置为无。然后,首席财务官可以自由地读写数据库,集团审计员的所有成员都可以读取数据库,其他人根本无法访问数据库。
这种配置是有效的,但很不灵活。如果审计员也是公司运动队的成员,并且运动队使用组类权限来控制对即将到来的赛程数据库的访问,会发生什么情况?审计员是否必须离开审计员组并加入运动队组,从而失去对帐户数据库的访问权限?实际上,否,因为用户不仅具有主要组(primary group),而且可以具有补充组(supplementary groups)。当进行访问控制决策时,如果被访问对象的组与用户的任何组匹配,则操作系统将使用组类权限。因此,用户可以是审核员组和运动队组的成员。用户不仅可以获取其任何补充组的权限,还可以将他们拥有的任何文件的组更改为其任何补充组。
我需要介绍的最后一个概念是设置组程序(set-group programs)。假设我们不希望每个用户都能够看到财务数据库,但我们同时也希望每个用户都能够获得其部门的余额报告。我们可以创建一个程序来检查哪个用户启动了该程序并获取该用户的余额报告。但这显然行不通,因为被运行程序会继承运行它们的用户的权限,而该用户无法读取数据库。因此,我们将程序设置组设置为“审计员”,那么现在当它运行时,它将承担审计员组的权限并能够读取财务数据库。(设置用户程序(set-user programs)也有类似的功能,但我不会在这里讨论)。
负组权限
在上面的示例中,所有者类权限 (r+w) 大于所属组类权限 (r),而所属组又大于其他类(无)。访问控制通常的限制方式是这样的:更为具体的类至少拥有与不太具体的类一样多的权限。但是,这不是固定不变的。例如,如果将对象设置为所有者类:r、所属组:无和 其他:r,那么就是除对象所属组的成员外,每个人都可以读取此文件。这是 Linux 权限的一个有意功能,并且确实被使用,尽管使用的不是很频繁。它可能有用的地方是为敏感对象建立拒绝列表,以便令某些不可信的程序无法访问它。
为了支持此类方案,用户应当不能删除其所在的组,因为一旦删除该用户就可以绕过负组权限强制实施的访问控制。那么可以通过设置属组可执行文件来执行此操作吗?比如某用户创建一个程序,将其所属组更改为用户的补充组之一,然后运行此程序。当程序运行时,其属组将是该用户的补充组,而不是该用户的主要组。**那么在这样情况下,该用户是否可以删除主组并访问其组不允许的对象呢?
幸运的是,该用户并不能访问。因为当用户登录时,其主要组将被复制并添加到补充组列表中。当设置组程序运行时,校验的组确实是其补充组,但补充组列表包括主要组。因此,操作系统将拒绝访问请求,因为(原本)被拒绝访问的主要组位于补充组列表中。但是,如果设置组程序在容器中运行,那么情况将会有所不同。
主组在容器中不备份
在登录系统时备份(添加)用户主要组到补充组列表的行为是在 1994 年发布的4.4BSD 操作系统中引入的,Linux 采用了类似的方法。但是,Docker 容器在设置主组时并没有遵循这一点,因此主要组不会被备份在补充组列表中出现。开放容器计划为容器创建了一个可互操作的标准,但该标准没有明确说明容器应该做什么。甚至,就我所知道的大多数容器在实现时都效仿了 Docker 。所以,在容器中运行的程序可能会删除其主要组,如果恰好正在使用负组权限,则程序将能够违反系统的访问控制策略。
利用此漏洞
如果攻击者可以在容器中运行代码,则可以创建一个设置组程序,并将该组设置为攻击者的一个补充组。当此程序运行时,其组将是攻击者的补充组,补充组列表将是该用户的补充组。但是,用户的主要组并不会出现在该程序的补充组列表当中。如果该程序访问原本对攻击者的主要组具有负组权限的文件,它将成功访问该文件,而这是违反负组权限预期的
可以在下面的视频中找到此漏洞的演示,该视频使用了我在 Github 上提供的POC。我创建的容器镜像位于 Docker Hub 上,适用于 x86_64 和 aarch64。
缓解问题
我在2022 年 5 月 27日向 Docker 报告了此漏洞,但对此漏洞的研究已传递给 Moby 项目,因为它影响的不仅仅是 Docker ,还包括多个Linux 容器。 受影响软件的开发人员一直在开发补丁,以修复漏洞并解决规范中的遗漏之处。由于该漏洞仅影响不常用的访问控制功能,所以该漏洞的严重性被评估为较低,因此可以在出现缓解方法后公开讨论该漏洞。
对于受到影响的软件包已分配CVE来跟踪该漏洞。为什么有多个 CVE ID?因为开放容器计划运行(Open Container Initiative Runtime)规范并不要求存在这种易受攻击的机制,但由于规范太过于模糊,于是乎便允许了这种行为的存在。尽管目前此规范的所有实现都容易受到攻击,但 CVE ID 也只是与易受攻击的软件相关联,而不是连接到规范。我希望规范也会被修改,用明确要求实现强制执行正确的行为。到目前为止分配的 CVE ID 为:
- CVE-2022-2989: podman
- CVE-2022-2990: buildah
- CVE-2022-2995: cri-o
- CVE-2022-36109: Moby (Docker Engine)
但是,与此同时,可以采取一些步骤来解决该漏洞。
使用“su”初始化容器
该漏洞是因为容器运行时未正确设置在容器配置中指定的组(即User Dockerfile指令和runAsUser/runAsGroup Kubernetes设置)。
存在一种方法是以root用户身份启动容器,然后使用带有“-l”标志的“su”命令来设置用户。这种方法将正确地建立组,从而避免问题。
手动备份(添加)组
容器运行时使用/etc/passwd和/etc/group的内容分别设置主组和辅助组。
如果您修改/etc/group以第二次将用户添加到它们的主组中,容器运行时将正确地设置这些组。
这种方法似乎有点老生常谈,但确实很管用。
例如,在这里,用户的主组同时在/etc/passwd(带有数字组ID 1000)和/etc/group中指定,其中该用户也被列为成员。
/etc/passwd: user:x:1000:1000:Linux User,,,:/home/user:/bin/ash
/etc/group: user:x:1000:user
阻止setuid/setgid程序
该漏洞依赖于具有更改可用权限的设置组程序。
如果您不需要setgid或setuid程序,您可以通过Docker“--Security-opt no-new-vilileses”标志或Kubernetes“AllowPrivilegeEscalation=False”设置禁用此功能。
识别那些对象使用了负组权限
很难知道软件是否依赖于负组权限,因为这是一个细节,通常没有明确的文档记录。
在研究开始时,我编写了一个程序来搜索具有负组权限的文件和目录。
它并不完美-它不会识别为临时文件设置负组权限的情况。
它也不会发现在配置文件中(例如,对于sudo)而不是在文件系统上实现负组权限的情况,也可能存在误报。
系统设计的经验教训
当然了,这个漏洞在被修复的同时,我们也可以从中吸取一些更广泛的教训。
首先 ,就本次而言,将用户的主要组包括在补充组中并没有作为一项要求写下来的-它是未定义(具体内容)的。而“未定义”的意思是应用程序不依赖于特定的行为,但这也不意味着所有的选择都是同样正确的。
其次 ,在互操作性标准(Interoperability standards)中有这样一个问题:它们规定了最低限度来实现最大程度的灵活性,但其安全性显然并不是您想要的。EMV卡支付互操作性标准也存在同样的弱点。
所以对于这种现状,无论是出于安全或其他原因都应该要求出具具体标准去规范其行为。
另外,还有一些关于软件重写的课题,比如目前为了确保内存安全而流行采用Rust语言去重写C语言程序。这样做确实可以带来实质性的安全好处,但任何重写都有可能丢失某些基本功能,特别是当这些基本功能只存在遗留代码当中时。比如,Docker有效地采用Go重写了组的初始化代码,也确实避免了原本C中的许多潜在缺陷,但由于没有采用与Linux相同的方式实现登录序列,因此又创建了一个新的漏洞。
当然,这并不是说重写是一个坏主意,只是这样做可能会带来新的风险,应该注意减轻。