Proof-of-concept e lab per un SSRF di Gitaly fetch in GitLab CE 19.4.1: gli importer GitHub/Bitbucket saltano il pinning di resolved_address, consentendo a un utente autenticato di effettuare il rebinding DNS e raggiungere host interni.
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · gitlab-gitaly-fetch-ssrf
GitLab CE 19.4.1 - GitLab Inc.
L'importazione da GitHub Enterprise / Bitbucket Server (e Direct Transfer, quando quel flag è attivo) salta Projects::ImportService#get_resolved_address. Repository#fetch_as_mirror imposta resolved_address: "" come default. Gitaly FetchRemote / CreateRepositoryFromURL risolve di nuovo l'hostname del clone. UrlBlocker aveva già consentito un record A pubblico. Rebind, e gli oggetti git-HTTP finiscono nel progetto importato.
Un utente autenticato che può eseguire l'importazione da GitHub Enterprise può far eseguire a Gitaly un git-fetch verso un host che non è più l'IP fissato da UrlBlocker.
| ID | nessuna CVE al momento |
| 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 |
| Prodotto | GitLab CE |
| Affected | 19.4.1-ce.0 (26212baacadb sull'albero v19.4.1-ee) |
| Auth | utente autenticato, importatore GitHub Enterprise / Bitbucket Server |
| Licenza | GNU Affero GPL v3.0 |
| Lab | solo 127.0.0.1 |
Punta l'importazione da GitHub Enterprise verso un hostname che controlla. UrlBlocker.validate! di Rails consente un record A pubblico. ParallelImporter.imports_repository? è true, quindi ImportService non fissa mai l'IP. RepositoryImporter#fetch_as_mirror chiama Gitaly con un resolved_address vuoto. Gitaly esegue un HTTP-fetch con git verso qualunque cosa quel nome risolva ora. Il repository importato contiene gli oggetti.
Su un'installazione omnibus all-in-one quello è il loopback del worker. Gli hostname di clone diretti http://127.0.0.1 falliscono in modo sicuro. L'importazione repository-by-URL fissa già l'IP. Direct Transfer richiede bulk_import_enabled (default false su self-managed).
Stesso prodotto, residui affini: Gitea HTTPS import pin drop, webhook 0.0.0.0/8.
Ho letto le note di patch della 19.4.1, poi gli importer. Projects::ImportService#get_resolved_address riscrive l'hostname con l'IP validato e lo passa a Gitaly. Gli importer paralleli di GitHub e Bitbucket Server impostano imports_repository? e saltano quel percorso.
project.repository.fetch_as_mirror(project.unsafe_import_url, refmap: refmap, forced: true)
fetch_as_mirror imposta resolved_address: "" come default. Il repository_pipeline.rb di Direct Transfer chiama persino validate! e scarta il valore di ritorno.
Ho avviato l'immagine stock gitlab/gitlab-ce:19.4.1-ce.0. In CE import_sources parte vuoto; il lab abilita GitHub. POST /api/v4/import/github con github_hostname=http://ghe.lab:18080. Un catcher nella netns di GitLab parla il finto JSON di GHE più git http-backend. Il blob WITNESS del repo seed è GITLAB-GITALY-FETCH-SSRF-WITNESS.
GET /api/v4/projects/1/repository/files/WITNESS/raw?ref=master ha restituito il witness. Log di Gitaly: FetchRemote grpc.code=OK su root/ghe-ssrf. Catcher: git/2.55 info/refs e git-upload-pack. Il caso negativo github_hostname=http://127.0.0.1:18080 restituisce 400 Invalid URL.
Vicoli ciechi già registrati: il seed CveLabRoot9! viene rifiutato da WeakPasswords (sottostringa root); la 19.4 supera quel controllo a 64 caratteri; Faraday non riesce a connettersi a un IP pubblico che non sia il catcher, quindi l'harness REDIRECTa 1.1.1.1:18080 mentre UrlBlocker vede ancora un A pubblico; git-HTTP può vincere prima che GET /repos/org/repo cambi il DNS; GitLab risponde 502 a metà importazione e poi il file è comunque lì. Una reverse shell. Teatro. L'oracolo è il blob importato più FetchRemote di Gitaly.
cd lab
./run.sh
Target solo http://127.0.0.1:18410. GitLab CE richiede diversi GB di RAM. Solo lab autorizzato.
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)
Passa get_resolved_address a fetch_as_mirror come fa già l'importazione repository-by-URL. Non scartare UrlBlocker.validate! nemmeno in Direct Transfer.
repository_importer.rb · repository.rb · import_service.rbGNU Affero GPL v3.0. Vedi LICENSE. Solo lab su loopback. Nessuna garanzia.