abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · gitlab-webhook-0net-ssrf
技术分析文章:https://abraxaslabs.tech/research/gitlab-webhook-0net-ssrf
GitLab CE 19.4.1 - GitLab Inc.
Gitlab::HTTP_V2::UrlBlocker 会拦截精确的 0.0.0.0、:: 和 127.0.0.0/8。但它不会拦截 0.0.0.0/8 中的其余地址。Linux 将该范围视为“本网络”,并会将 0.0.0.1 投递到本地协议栈。在 allow_local_requests_from_web_hooks_and_services 关闭的情况下,项目 Maintainer 可以创建一个指向 http://0.0.0.1:<port>/ 的 webhook。GitLab 会在 hook 日志中存储响应内容的 8 KB。
项目自身的 Maintainer 可以让 GitLab 通过 0.0.0.1 向回环地址发起 POST 请求,并从 hook 日志中读回响应体。
| ID | 暂无 CVE |
| CWE | CWE-918 |
| CVSS | 高危:8.5 CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:N |
| 产品 | GitLab CE |
| 受影响版本 | 19.4.1-ce.0(v19.4.1-ee 树上的 26212baacadb) |
| 认证 | 已认证的 Maintainer(admin_web_hook) |
| 许可证 | GNU Affero GPL v3.0 |
| 实验环境 | 仅 127.0.0.1 |
使用 url=http://0.0.0.1:<port>/ 调用 POST /api/v4/projects/:id/hooks。触发或测试。从 hook 事件 API 读取 response_body。在 all-in-one omnibus 部署中,Sidekiq 与 Puma、Workhorse 以及各 exporter 共享容器网络命名空间。其项目的 Maintainer 不应读取这些内容。
http://127.0.0.1/ 和 http://169.254.169.254/ 仍会被拦截。这不是通过链路本地地址访问 IMDS。这是“本网络”漏洞。
同一产品的其他遗留问题:Gitea HTTPS 导入固定地址丢弃、Gitaly fetch 省略 resolved_address。
在 19.4.1 的 SSRF 修复之后,我阅读了 url_blocker.rb。validate_localhost 只精确匹配 "::" 和 "0.0.0.0",再加上 Socket.ip_address_list。validate_loopback 则是 ipv4_loopback?(127.0.0.0/8)和 ipv6 回环。测试用例覆盖了 127.0.0.1 的八进制/十六进制别名。它们从未提及 0.0.0.1。
Webhook 是响应体的汇聚点:WebHookService 发起 POST 并存储 8 KB。Maintainer 拥有 admin_web_hook 权限。
我启动了原版 gitlab/gitlab-ce:19.4.1-ce.0。本地请求已关闭。在 GitLab 网络命名空间内于 0.0.0.1:18080 上运行 catcher。创建指向 http://127.0.0.1:18080/ 和 http://169.254.169.254/ 的 hook:返回 422 Invalid url given。创建指向 http://0.0.0.1:18080/ 的 hook:返回 201。测试 push 事件:返回 201。hook 事件的 response_body 中包含 GITLAB-WEBHOOK-0NET-SSRF-WITNESS。
已记录的弯路:带 Accept: application/json 访问 /users/sign_in 返回 404;种子密码包含 root 会触发 WeakPasswords;将 0.0.0.1 添加到 lo 会使其出现在 Socket.ip_address_list 中,因此 validate_localhost 会对这个“阳性”用例返回 422;IP_FREEBIND 加上 ip route add local 0.0.0.1/32 dev lo 可以在不加入地址列表的情况下完成绑定;compose 中密码里的 $ 需要写成 $$。反弹 shell。纯属表演。判定依据是:0.0.0.1 返回 201,127.0.0.1 返回 422,日志中出现见证字符串。
cd lab
./run.sh
仅针对 http://127.0.0.1:18420。GitLab CE 需要数 GB 内存。仅限授权实验环境。
negative-a 127.0.0.1 blocked
negative-b link-local blocked
positive-hook id=1
SUCCESS GITLAB-WEBHOOK-0NET-SSRF webhook response_body from 0.0.0.1:18080 hook_id=1 GITLAB-WEBHOOK-0NET-SSRF-WITNESS
def validate_localhost(addrs_info)
local_ips = ["::", "0.0.0.0"]
local_ips.concat(Socket.ip_address_list.map(&:ip_address))
return if (local_ips & addrs_info.map(&:ip_address)).empty?
raise BlockedUrlError, "Requests to localhost are not allowed"
end
def validate_loopback(addrs_info)
return unless addrs_info.any? { |addr| addr.ipv4_loopback? || addr.ipv6_loopback? }
raise BlockedUrlError, "Requests to loopback addresses are not allowed"
end
像拒绝 127.0.0.0/8 那样拒绝 0.0.0.0/8(以及 ::ffff:0.0.0.0/104)。
url_blocker.rb · web_hook_service.rb · project_hooks.rbGNU Affero GPL v3.0。参见 LICENSE。仅限回环实验环境。不提供任何担保。