abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · gitlab-gitea-import-ssrf
技术分析文章:https://abraxaslabs.tech/research/gitlab-gitea-import-ssrf
GitLab CE 19.4.1 - GitLab Inc.
Gitea 导入只调用一次 UrlBlocker.validate!,随后对于 https 会丢弃已固定 IP 的 URI,并把原始主机名交给 Faraday。Octokit 的 UrlValidation 会再次校验,但不会重写 env[:url]。在这两次查询之间重新绑定域名,GitLab 就会像访问 Gitea 一样对回环地址发起 GET 请求。仓库列表 JSON 会通过 GET /import/gitea/status.json 返回。HTTP 导入会保留 IP 固定。直接访问 https://127.0.0.1 会被阻止。
一个已登录用户,只要能在其个人命名空间打开 Gitea 导入,就能让 GitLab 请求一个被其重新绑定到回环地址的主机,并获取响应正文。
| ID | 暂无 CVE |
| CWE | CWE-918、CWE-367 |
| CVSS | 高危: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) |
| 认证 | 已认证用户、Gitea 导入、个人命名空间 |
| 许可证 | GNU Affero GPL v3.0 |
| 实验环境 | 仅 127.0.0.1 |
将 Gitea Host URL 设置为 https://<攻击者控制的域名>/。UrlBlocker 会放行第一条公网 A 记录。HTTPS 的 client_options 会保留该主机名。随后将其重新绑定到 127.0.0.1。Faraday 会在 GitLab worker 的回环地址上向 catcher 发起 GET 请求。status.json 中的 provider_repos 就是 catcher 返回的内容。
在 all-in-one omnibus 部署中,这包括 Workhorse、Puma、各类 exporter,以及 worker 上监听的任何其他服务。这不是主机 RCE,也不是未认证攻击。HTTP Host URL 仍会保留在已固定的 IP 上。
同一产品中的同类遗留问题:Gitaly fetch 省略 resolved_address、webhook 0.0.0.0/8。
我先阅读了 2026 年 9 月 23 日的 19.4.1 / 19.3.3 / 19.2.7 补丁说明,然后查看了 v19.4.1-ee 代码树。CI 正则 RCE、MR diff XSS、Workhorse file.path 读取在该版本上均已修复。导入 SSRF 尚未处理完毕。
Import::GiteaController#client_options 就是遗留点:
host: verified_url.scheme == 'https' ? provider_url : verified_url.to_s,
HTTP 使用 verified_url(IP)。HTTPS 使用 provider_url(用户输入的名称)。LegacyGithubImport::Client 基于该主机构建 Octokit 的 api_endpoint。Gitlab::Octokit::UrlValidation 先调用 validate!,然后调用 @app.call(env),不做任何重写。
我启动了原版 gitlab/gitlab-ce:19.4.1-ce.0。本地请求已关闭。由于 Faraday 的 ssl.verify 为 true,实验用 CA 放在 /etc/gitlab/trusted-certs 中。gitea.lab 的 DNS:第 1-2 次查询返回 1.1.1.1,之后的查询返回 127.0.0.1。捕获器与 GitLab 共享 netns,监听 :443。GET /import/gitea/status.json 返回了名为 GITLAB-GITEA-IMPORT-SSRF-WITNESS 的 provider_repos。捕获器日志:GET /api/v1/user/repos peer=127.0.0.1 host=gitea.lab。
直接访问 https://127.0.0.1/:提示 "Specified URL cannot be used"。HTTP http://gitea.lab/ 仍停留在固定地址上,未返回见证标记。
已记录的弯路:带 Accept: application/json 访问 /users/sign_in 返回 404(HTML 返回 200);种子密码 CveLabRoot9! 因包含 root 被拒绝;CA 以 ro 方式绑定挂载后,重新配置会链接该文件,导致 EROFS;在第 2 次查询时先返回前两个地址再返回回环地址,会使 UrlValidation 在 Faraday 连接之前就拦截。最终奏效的是成对模式(奇数次返回公网地址,偶数次返回回环地址)。一个反向 shell。一场戏。判定依据是 status.json 中的见证标记加上捕获器的对端地址。
cd lab
./run.sh
仅针对 http://127.0.0.1:18400。GitLab CE 需要数 GB 内存。首次启动较慢。仅限授权实验室环境。
status.json status=200 ... provider_repos ... GITLAB-GITEA-IMPORT-SSRF-WITNESS
catcher GET /api/v1/user/repos peer=127.0.0.1 host=gitea.lab status=200 witness=True
negative-a https://127.0.0.1 blocked
negative-b http://gitea.lab no-witness
SUCCESS GITLAB-GITEA-IMPORT-SSRF status.json mode=pairs http=200 GITLAB-GITEA-IMPORT-SSRF-WITNESS
def client_options
verified_url, provider_hostname = verify_blocked_uri
{
host: verified_url.scheme == 'https' ? provider_url : verified_url.to_s,
api_version: 'v1',
hostname: provider_hostname
}
end
对 https 使用 verified_url.to_s,方式与 HTTP 已有的做法相同。在 Octokit 的 UrlValidation 中将 env[:url] 重写为已固定的 IP。
gitea_controller.rb · legacy_github_import/client.rb · octokit/url_validation.rbGNU Affero GPL v3.0。参见 LICENSE。仅限回环实验环境。不提供任何担保。