
Proof-of-concept и лабораторная работа для SSRF через вебхук GitLab CE с использованием диапазона this-network 0.0.0.0/8, позволяющая Maintainer'у получить доступ к loopback и читать ответы из логов хука.
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · 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 в локальный стек. Maintainer проекта может создать webhook на http://0.0.0.1:<port>/, пока allow_local_requests_from_web_hooks_and_services выключен. GitLab сохраняет 8 КБ ответа в логах хука.
Maintainer своего собственного проекта может заставить GitLab выполнить POST на loopback через 0.0.0.1 и прочитать тело ответа из лога хука.
| ID | CVE пока нет |
| CWE | CWE-918 |
| CVSS | High: 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 (26212baacadb в дереве v19.4.1-ee) |
| Аутентификация | аутентифицированный Maintainer (admin_web_hook) |
| Лицензия | GNU Affero GPL v3.0 |
| Лаборатория | только 127.0.0.1 |
POST /api/v4/projects/:id/hooks с url=http://0.0.0.1:<port>/. Триггер или тест. Чтение response_body из API событий хука. В all-in-one omnibus Sidekiq делит сетевой namespace контейнера с Puma, Workhorse и экспортёрами. Maintainer своего проекта не должен их читать.
http://127.0.0.1/ и http://169.254.169.254/ остаются заблокированными. Это не IMDS через link-local. Это дыра в «этой сети».
Тот же продукт, родственные остатки: Gitea HTTPS import pin drop, Gitaly fetch omits resolved_address.
Я прочитал url_blocker.rb после исправлений SSRF в 19.4.1. validate_localhost — это точные "::" и "0.0.0.0" плюс Socket.ip_address_list. validate_loopback — это ipv4_loopback? (127.0.0.0/8) и ipv6 loopback. Спецификации покрывают восьмеричные/шестнадцатеричные алиасы 127.0.0.1. Они никогда не упоминают 0.0.0.1.
Webhook — это приёмник тела: WebHookService выполняет POST и сохраняет 8 КБ. У Maintainer есть admin_web_hook.
Я поднял стандартный gitlab/gitlab-ce:19.4.1-ce.0. Локальные запросы выключены. Перехватчик внутри сетевого namespace GitLab на 0.0.0.1:18080. Создание хука http://127.0.0.1:18080/ и http://169.254.169.254/ : 422 Invalid url given. Создание хука http://0.0.0.1:18080/ : 201. Тест событий push : 201. response_body событий хука содержит GITLAB-WEBHOOK-0NET-SSRF-WITNESS.
Уже зафиксированные неверные повороты: /users/sign_in с Accept: application/json даёт 404; seed-пароль, содержащий 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 требует $$. Обратная оболочка. Театр. Оракул — это 201 на 0.0.0.1, 422 на 127.0.0.1, свидетель в логе.
cd lab
./run.sh
Цель только http://127.0.0.1:18420. GitLab CE требует несколько ГБ RAM. Только авторизованная лаборатория.
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
Запретить 0.0.0.0/8 (и ::ffff:0.0.0.0/104) так же, как запрещён 127.0.0.0/8.
url_blocker.rb · web_hook_service.rb · project_hooks.rbGNU Affero GPL v3.0. См. LICENSE. Только loopback-лаборатория. Без гарантий.