abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · gitlab-gitaly-fetch-ssrf
GitLab CE 19.4.1 - GitLab Inc.
GitHub Enterprise / Bitbucket Server 导入(以及启用该标志时的 Direct Transfer)会跳过 Projects::ImportService#get_resolved_address。Repository#fetch_as_mirror 默认 resolved_address: ""。Gitaly 的 FetchRemote / CreateRepositoryFromURL 会重新解析克隆主机名。UrlBlocker 此前已允许了一个公共 A 记录。重新绑定后,git-HTTP 对象就会落入被导入的项目中。
一个已登录、且能执行 GitHub Enterprise 导入的用户,可以让 Gitaly 对某个主机执行 git-fetch,而该主机已不再是 UrlBlocker 所固定的那个 IP。
| ID | 暂无 CVE |
| CWE | CWE-918、CWE-367 |
| CVSS | High: 7.7 CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:L/A:N |
| 产品 | GitLab CE |
| 受影响版本 | 19.4.1-ce.0(v19.4.1-ee 树上的 26212baacadb) |
| 认证 | 已认证用户,GitHub Enterprise / Bitbucket Server 导入器 |
| 许可证 | GNU Affero GPL v3.0 |
| 实验环境 | 仅 127.0.0.1 |
将 GitHub Enterprise 导入指向其控制的主机名。Rails 的 UrlBlocker.validate! 允许了一个公共 A 记录。ParallelImporter.imports_repository? 为 true,因此 ImportService 从不固定地址。RepositoryImporter#fetch_as_mirror 以空的 resolved_address 调用 Gitaly。Gitaly 的 git 会 HTTP-fetch 该名称当前指向的任何地址。被导入的仓库中就包含了这些对象。
在全能 omnibus 上,那就是 worker 的 loopback。直接使用 http://127.0.0.1 克隆主机名会失败关闭。通过 URL 导入仓库已经会固定地址。Direct Transfer 需要 bulk_import_enabled(自管理默认 false)。
同一产品的同类遗留问题:Gitea HTTPS 导入固定地址丢失、webhook 0.0.0.0/8。
我阅读了 19.4.1 的补丁说明,然后看了导入器。Projects::ImportService#get_resolved_address 会将主机名重写为已验证的 IP,并将其传入 Gitaly。GitHub 和 Bitbucket Server 并行导入器设置了 imports_repository? 并跳过该路径。
project.repository.fetch_as_mirror(project.unsafe_import_url, refmap: refmap, forced: true)
fetch_as_mirror 默认 resolved_address: ""。Direct Transfer 的 repository_pipeline.rb 甚至调用了 validate! 却丢弃了返回值。
我搭建了原版 gitlab/gitlab-ce:19.4.1-ce.0。CE 的 import_sources 初始为空;实验环境启用了 GitHub。POST /api/v4/import/github 携带 github_hostname=http://ghe.lab:18080。GitLab netns 中的捕获器会响应伪造的 GHE JSON 以及 git http-backend。种子仓库 blob WITNESS 为 GITLAB-GITALY-FETCH-SSRF-WITNESS。
GET /api/v4/projects/1/repository/files/WITNESS/raw?ref=master 返回了该见证值。Gitaly 日志:FetchRemote 在 root/ghe-ssrf 上 grpc.code=OK。捕获器:git/2.55 的 info/refs 和 git-upload-pack。反向测试 github_hostname=http://127.0.0.1:18080 返回 400 Invalid URL。
已记录的弯路:种子 CveLabRoot9! 会被 WeakPasswords 拒绝(包含 root 子串);19.4 会在 64 字符处填充以绕过该检查;Faraday 无法连接到不是捕获器的公共 IP,因此测试框架将 1.1.1.1:18080 REDIRECT,而 UrlBlocker 仍看到公共 A 记录;git-HTTP 可能在 GET /repos/org/repo 翻转 DNS 之前就成功;GitLab 在导入中途返回 502,但之后文件仍然存在。一个反向 shell。纯属表演。判定依据是被导入的 blob 加上 Gitaly 的 FetchRemote。
cd lab
./run.sh
目标仅限 http://127.0.0.1:18410。GitLab CE 需要数 GB 内存。仅限授权实验环境。
negative-loopback status=400 Invalid URL
witness-import status=201 import_status=scheduled
file-final http=200 body='GITLAB-GITALY-FETCH-SSRF-WITNESS'
SUCCESS GITLAB-GITALY-FETCH-SSRF imported-witness git-fetch-no-pin GITLAB-GITALY-FETCH-SSRF-WITNESS
def import_repository
project.ensure_repository
refmap = Gitlab::GithubImport.refmap
project.repository.fetch_as_mirror(project.unsafe_import_url, refmap: refmap, forced: true)
像通过 URL 导入仓库已经做的那样,将 get_resolved_address 传入 fetch_as_mirror。也不要在 Direct Transfer 中丢弃 UrlBlocker.validate!。
repository_importer.rb · repository.rb · import_service.rbGNU Affero GPL v3.0。参见 LICENSE。仅限 loopback 实验环境。无任何担保。