Prova de conceito e laboratório para um SSRF de fetch do Gitaly no GitLab CE 19.4.1: os importadores do GitHub/Bitbucket ignoram a fixação de resolved_address, permitindo que um usuário autenticado faça rebinding de DNS e acesse hosts internos.
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · gitlab-gitaly-fetch-ssrf
GitLab CE 19.4.1 - GitLab Inc.
A importação do GitHub Enterprise / Bitbucket Server (e a Transferência Direta, quando essa flag está ativada) ignora Projects::ImportService#get_resolved_address. Repository#fetch_as_mirror usa resolved_address: "" por padrão. O Gitaly FetchRemote / CreateRepositoryFromURL resolve o hostname do clone novamente. O UrlBlocker já havia permitido um registro A público. Faça o rebind, e os objetos git-HTTP chegam ao projeto importado.
Um usuário autenticado que pode executar a importação do GitHub Enterprise pode fazer o Gitaly git-fetch de um host que não é mais o IP fixado pelo UrlBlocker.
| ID | ainda sem CVE |
| CWE | CWE-918, CWE-367 |
| CVSS | Alto: 7.7 CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:L/A:N |
| Produto | GitLab CE |
| Afetado | 19.4.1-ce.0 (26212baacadb na árvore v19.4.1-ee) |
| Autenticação | usuário autenticado, importador do GitHub Enterprise / Bitbucket Server |
| Licença | GNU Affero GPL v3.0 |
| Lab | apenas 127.0.0.1 |
Aponte a importação do GitHub Enterprise para um hostname que ele controla. O UrlBlocker.validate! do Rails permite um registro A público. ParallelImporter.imports_repository? é verdadeiro, então o ImportService nunca fixa. RepositoryImporter#fetch_as_mirror chama o Gitaly com um resolved_address vazio. O Gitaly faz HTTP-fetch com git do que esse nome for agora. O repositório importado contém os objetos.
Em omnibus all-in-one, isso é o loopback do worker. Hostnames de clone diretos http://127.0.0.1 falham de forma segura. A importação por URL de repositório já fixa. A Transferência Direta precisa de bulk_import_enabled (padrão false em self-managed).
Mesmo produto, sobras irmãs: Gitea HTTPS import pin drop, webhook 0.0.0.0/8.
Li as notas de patch da 19.4.1, depois os importadores. Projects::ImportService#get_resolved_address reescreve o hostname para o IP validado e o passa ao Gitaly. Os importadores paralelos do GitHub e Bitbucket Server definem imports_repository? e pulam esse caminho.
project.repository.fetch_as_mirror(project.unsafe_import_url, refmap: refmap, forced: true)
fetch_as_mirror usa resolved_address: "" por padrão. O repository_pipeline.rb da Transferência Direta ainda chama validate! e descarta o valor de retorno.
Subi o gitlab/gitlab-ce:19.4.1-ce.0 padrão. O import_sources do CE começa vazio; o lab habilita o GitHub. POST /api/v4/import/github com github_hostname=http://ghe.lab:18080. Catcher no netns do GitLab fala JSON falso de GHE mais git http-backend. O blob WITNESS do repositório semente é GITLAB-GITALY-FETCH-SSRF-WITNESS.
GET /api/v4/projects/1/repository/files/WITNESS/raw?ref=master retornou o witness. Log do Gitaly: FetchRemote grpc.code=OK em root/ghe-ssrf. Catcher: git/2.55 info/refs e git-upload-pack. O negativo github_hostname=http://127.0.0.1:18080 é 400 Invalid URL.
Caminhos errados já registrados: a semente CveLabRoot9! é rejeitada por WeakPasswords (substring root); a 19.4 preenche além dessa checagem em 64 caracteres; o Faraday não consegue conectar a um IP público que não seja o catcher, então o harness REDIRECTa 1.1.1.1:18080 enquanto o UrlBlocker ainda vê um A público; o git-HTTP pode vencer antes de GET /repos/org/repo virar o DNS; o GitLab dá 502 no meio da importação e depois o arquivo está lá mesmo assim. Um reverse shell. Teatro. O oráculo é o blob importado mais o FetchRemote do Gitaly.
cd lab
./run.sh
Alvo apenas http://127.0.0.1:18410. O GitLab CE quer vários GB de RAM. Apenas lab autorizado.
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)
Passe get_resolved_address para fetch_as_mirror do jeito que a importação por URL de repositório já faz. Não descarte UrlBlocker.validate! na Transferência Direta também.
repository_importer.rb · repository.rb · import_service.rbGNU Affero GPL v3.0. Veja LICENSE. Apenas lab em loopback. Sem garantia.