
Proof-of-concept и лабораторная работа для SSRF в Gitaly fetch GitLab CE 19.4.1: импортёры GitHub/Bitbucket пропускают привязку resolved_address, позволяя аутентифицированному пользователю перепривязать DNS и получить доступ к внутренним хостам.
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-запись. Выполните rebind — и git-HTTP-объекты окажутся в импортированном проекте.
Аутентифицированный пользователь, имеющий возможность запускать импорт из GitHub Enterprise, может заставить Gitaly выполнить git-fetch с хоста, который уже не соответствует IP, закреплённому UrlBlocker.
| 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 (26212baacadb в дереве v19.4.1-ee) |
| Аутентификация | аутентифицированный пользователь, импортёр 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 вызывает Gitaly с пустым resolved_address. Gitaly выполняет HTTP-fetch через git по тому, чем это имя является сейчас. Импортированный репозиторий содержит эти объекты.
В конфигурации all-in-one omnibus это loopback воркера. Прямые имена хостов клонирования вида http://127.0.0.1 отклоняются по принципу fail closed. Импорт репозитория по URL уже закрепляет адрес. Для Direct Transfer требуется bulk_import_enabled (в self-managed по умолчанию false).
Тот же продукт, родственные недоделки: Gitea HTTPS import pin drop, 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: "". repository_pipeline.rb из Direct Transfer даже вызывает 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 отвечает поддельным JSON GHE и git http-backend. Блоб WITNESS в seed-репозитории — это GITLAB-GITALY-FETCH-SSRF-WITNESS.
GET /api/v4/projects/1/repository/files/WITNESS/raw?ref=master вернул witness. Лог Gitaly: FetchRemote grpc.code=OK для root/ghe-ssrf. Перехватчик: git/2.55 info/refs и git-upload-pack. Негативный github_hostname=http://127.0.0.1:18080 даёт 400 Invalid URL.
Уже зафиксированные ошибочные пути: seed CveLabRoot9! отклоняется WeakPasswords (подстрока root); 19.4 дополняет пароль до 64 символов и проходит эту проверку; Faraday не может подключиться к публичному IP, который не является перехватчиком, поэтому стенд делает REDIRECT на 1.1.1.1:18080, пока UrlBlocker всё ещё видит публичную A-запись; git-HTTP может успеть до того, как GET /repos/org/repo переключит DNS; GitLab отдаёт 502 в середине импорта, а затем файл всё равно оказывается на месте. Обратная оболочка. Театр. Оракулом служит импортированный блоб плюс Gitaly FetchRemote.
cd lab
./run.sh
Цель — только http://127.0.0.1:18410. GitLab CE требует несколько ГБ RAM. Только авторизованная лаборатория.
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)
Передайте get_resolved_address в fetch_as_mirror так, как это уже делает импорт репозитория по URL. Также не отбрасывайте UrlBlocker.validate! в Direct Transfer.
repository_importer.rb · repository.rb · import_service.rbGNU Affero GPL v3.0. См. LICENSE. Только loopback-лаборатория. Без гарантий.