!exploitable Episode Three - Devfile Adventures
引言
我知道我们已经多次提到过,但以防你刚刚开始关注,Doyensec团队之前在地中海游轮上进行公司团建时,为了打发派对之间的时间,我们进行了一些黑客分析会议,研究现实世界中的漏洞,由此诞生了!exploitable博客系列。
在第一部分中,我们介绍了进入IoT ARM漏洞利用的历程,而第二部分则讲述了我们尝试利用《黑客帝国:重装上阵》电影中Trinity所用漏洞的经历。
在本期节目中,我们将深入探讨GitLab中CVE-2024-0402的漏洞利用。就像洋葱一样,这个漏洞表面之下总有一层又一层的问题,从YAML解析器差异到解压缩函数中的路径遍历,最终实现在GitLab中的任意文件写入。
当时没有公开的Proof of Concept(PoC)发布,而制作PoC的过程证明是一次冒险,值得在原作者的博客文章基础上扩展,添加PoC相关信息来完整这个故事。
背景介绍
这个漏洞影响了GitLab的Workspaces功能。长话短说,该功能允许开发人员即时启动集成开发环境(IDE),所有依赖项、工具和配置都已准备就绪。
整个Workspaces功能依赖于几个组件,包括一个运行的Kubernetes GitLab Agent和一个devfile配置。
- Kubernetes GitLab Agent:Kubernetes GitLab Agent将GitLab连接到Kubernetes集群,允许用户启用部署流程自动化,并更轻松地集成GitLab CI/CD流水线。它还允许创建Workspaces。
- Devfile:这是一个定义容器化开发环境的开放标准。简单来说,它是通过YAML文件配置的,用于定义特定项目所需的工具、运行时和依赖项。
以下是一个devfile配置示例(需放置在GitLab仓库中,文件名为.devfile.yaml):
|
|
漏洞详情
让我们从公开可用的信息开始,并补充额外的代码上下文。
GitLab使用devfile Gem(当然是Ruby写的),调用外部的devfile二进制文件(用Go编写),以便在特定仓库中创建Workspace时处理.devfile.yaml文件。
在Workspaces应用的devfile预处理例程中,一个名为validate_parent的特定验证器在GitLab中被PreFlattenDevfileValidator调用。
|
|
但parent选项是什么?根据Devfile文档:
如果指定了父devfile,给定的devfile将从其父文件继承所有行为。不过,您仍然可以使用子devfile来覆盖父dev文件中的某些内容。
然后,它继续描述了三种类型的父引用:
- 通过注册表引用的父文件 - 远程devfile注册表
- 通过URI引用的父文件 - 静态HTTP服务器
- 通过Kubernetes资源标识的父文件 - 可用的命名空间
与任何其他远程获取功能一样,值得审查以发现漏洞。但乍一看,validate_parent似乎阻止了该选项。
利用YAML解析器差异取胜
众所周知,即使是特定标准最常用的实现,也可能与规范中定义的内容存在细微偏差。在这种特定情况下,需要利用Ruby和Go之间的YAML解析器差异。
原作者为我们的差异笔记提供了一个新技巧。在YAML规范中:
- 单感叹号
!用于自定义或应用程序特定的数据类型1my_custom_data: !MyType "some value" - 双感叹号
!!用于内置的YAML类型1bool_value: !!bool "true"
他发现本地YAML标签表示法!(RFC参考)在Ruby的yaml库中仍然会激活二进制格式的base64解码,而Go的gopkg.in/yaml.v3则会直接丢弃它,导致以下行为:
|
|
因此,可以通过validate_parent函数向GitLab传递一个包含parent选项的devfile,并使其在devfile二进制执行时被使用。
任意文件写入
此时,我们需要切换到在devfile二进制文件(Go实现)中发现的漏洞。
在深入研究了依赖的依赖的依赖之后,研究者找到了decompress函数。该函数从注册表的库中获取tar.gz存档,并将文件解压缩到GitLab服务器内部。之后,它应该将这些文件移动到已部署的Workspace环境中。
以下是getResourcesFromRegistry使用的易受攻击的解压缩函数:
|
|
该函数打开tarFile并通过tarReader.Next()遍历其内容。仅处理tar.TypeDir和tar.TypeReg类型的内容,防止符号链接和其他嵌套利用。
然而,target := path.Join(targetDir, filepath.Clean(header.Name))这一行存在路径遍历漏洞,原因如下:
header.Name来自由devfile注册表提供的远程tar存档。- 众所周知,
filepath.Clean无法防止相对路径上的路径遍历(../不会被移除)。
执行结果类似于:
|
|
有许多脚本可以创建利用这种目录遍历模式的有效恶意存档(例如evilarc.py)。
串联漏洞点
- devfile库中从远程注册表获取文件的解压缩问题,允许包含恶意
.tar存档的devfile注册表在devfile客户端系统内写入任意文件。 - 在GitLab中,开发人员可以制作一个看似错误但有效的
.devfile.yaml定义,其中包含parent选项,这将迫使GitLab服务器使用恶意注册表,从而在服务器本身上触发任意文件写入。
利用此漏洞的要求是:
- 能够以开发人员身份访问目标GitLab,并且有权向仓库提交代码。
- GitLab实例上正确配置了Workspaces功能(v16.8.0及以下版本)。
开始利用!
配置环境
为了确保你了解全貌,我必须告诉你在游轮上配置GitLab Workspaces是什么感觉,网速慢得要命——简直是噩梦!
当然,有关于如何操作的文档,但今天你将获得一些额外的发现:
- 请遵循GitLab 16.8的文档页面,而不是最新版本,因为它已经改变了。不要像我们一样,在海中央浪费了宝贵的娱乐时间。
- 该功能变化如此之大,他们甚至移除了GitLab 16.8所需的容器镜像。因此,你需要修补缺失的
web-ide-injector容器镜像。将1 2ubuntu@gitlabServer16.8:~$ find / -name "editor_component_injector.rb" 2>/dev/null /opt/gitlab/embedded/service/gitlab-rails/ee/lib/remote_development/workspaces/create/editor_component_injector.rbweb-ide-injector镜像的第129行的值替换为:1registry.gitlab.com/gitlab-org/gitlab-web-ide-vscode-fork/gitlab-vscode-build:latest - GitLab Agent必须具有
remote_development选项才能允许Workspaces。 以下是一个有效的config.yaml文件示例:1 2 3 4 5 6 7remote_development: enabled: true dns_zone: "workspaces.gitlab.yourdomain.com" observability: logging: level: debug grpc_level: warn
配置时,祝你好运。
开始制作
如前所述,这个漏洞链就像洋葱一样层层叠加。以下是2025年AI生成的经典图像,为我们勾勒了其轮廓:
公开可用的信息留给我们以下任务,如果我们想要利用它:
- 部署一个自定义的devfile注册表,按照原始仓库操作,结果证明这很容易。
- 通过包含
.tar文件使其变得恶意,该文件打包了我们的路径遍历,以覆盖GitLab实例中的某些内容。 - 在目标GitLab仓库中添加一个指向它的
.devfile.yaml。
为了找出恶意archive.tar的归属位置,我们必须退一步,多读一些代码。特别是,我们必须了解调用易受攻击的decompress函数的上下文。
我们最终阅读了PullStackByMediaTypesFromRegistry函数,该函数用于将具有允许媒体类型的指定堆栈从给定的注册表URL拉取到某个目标目录。
参见library.go:293:
|
|
代码模式强调了涉及devfile注册表堆栈,并且它们的结构中包含某个archive.tar文件。
为什么devfile堆栈要包含一个tar文件?
archive.tar文件可能包含在软件包中,用于分发启动器项目或预配置的应用程序模板。它帮助开发人员快速设置其工作区,包括示例代码、配置和依赖项。
在devfile注册表构建过程中进行一些快速的GitHub搜索后发现,我们的目标.tar文件应放置在注册表项目内的stacks/<STACK_NAME>/<STACK_VERSION>/archive.tar目录下,与包含要部署的特定版本的devfile.yaml的目录相同。
因此,我们自定义注册表中用于路径遍历的tar文件的目标位置是:
|
|
构建并运行恶意devfile注册表
构建我们的自定义注册表需要一些额外的工作(无法让构建脚本正常工作,不得不编辑它们),但我们最终成功将我们的archive.tar(例如,使用evilarc.py创建)放在了正确的位置,并制作了一个正确的index.json来提供它。最终可重复使用的结构可以在我们的PoC仓库中找到,所以可以节省一些时间来构建devfile注册表镜像。
运行恶意注册表的命令:
docker run -d -p 5000:5000 --name local-registrypoc registry:2用于运行一个本地容器注册表,该注册表将被devfile注册表用于存储实际的堆栈(见黄色高亮部分)。docker run --network host devfile-index用于运行使用官方仓库构建的恶意devfile注册表。可以在我们的PoC仓库中找到它。
触发漏洞 💥
一旦你有了一个目标GitLab实例可以访问的运行中的注册表,你只需要以开发人员身份在GitLab中认证,并编辑仓库的.devfile.yaml,利用之前展示的YAML解析器差异将其指向该注册表。以下是一个可以使用的示例:
|
|
要触发文件写入,只需在编辑后的仓库中启动一个新的Workspace并等待即可。
很好!我们成功地将Hello CVE-2024-0402!写入到了/tmp/plsWorkItsPartyTime.txt中。
后续利用方向
我们获得了写入权限,但我们不能就此止步,因此我们研究了一些可靠的升级方法。
首先,我们使用GitLab服务器上的会话来检查执行文件写入的系统用户。
|
|
显然,我们针对的用户是git,这是GitLab内部一个非常重要的用户。
检查可写文件以寻找快速突破口后,我们发现它似乎已经加固,没有大量可编辑的配置文件,正如预期的那样。
|
|
一些有趣的文件等待着被覆盖,但你可能已经注意到了最快但不太光彩的入口:/var/opt/gitlab/.ssh/authorized_keys。
值得注意的是,你可以将SSH密钥添加到你的GitLab账户,然后使用它以git用户身份进行SSH登录以执行与代码相关的操作。authorized_keys文件由GitLab Shell管理,它会从用户配置文件中添加SSH密钥,并将它们强制放入一个受限的shell中,以进一步管理/限制用户访问级别。
以下是将个人资料SSH密钥添加到GitLab时,添加到authorized_keys中的一行示例:
|
|
由于我们获得了任意文件写入权限,我们可以直接用包含我们可以使用的非受限密钥的authorized_keys文件替换原有的文件。回到我们的漏洞准备工作中,专门为此创建一个新的.tar文件:
|
|
此时,在你的恶意devfile注册表中替换archive.tar,重建其镜像并运行它。准备就绪后,通过在GitLab Web UI中创建一个新的Workspace再次触发漏洞。
几秒钟后,你应该能够以不受限制的git用户身份进行SSH登录。
下面我们还展示了如何更改GitLab Web根用户密码:
|
|
最后,你就可以在目标Web实例中以root用户身份进行认证了。
结论
我们的目标是为CVE-2024-0402构建一个PoC。尽管时间和连接受限,我们还是做到了。尽管如此,在准备GitLab Workspaces环境时,我们遇到了大量的配置错误,由于设置后功能本身根本无法工作,我们几乎要放弃了。这再次证明了,由于配置时间限制,只有少数人涉足的地方可能会发现非常好的漏洞。
向漏洞发现者joernchen致敬。
不仅漏洞本身很棒,而且他在这篇文章中还出色地描述了他所遵循的研究路径。我们很享受利用它的过程,并希望人们能通过我们公开的漏洞利用节省时间!