Prueba de concepto y laboratorio para un SSRF de fetch de Gitaly en GitLab CE 19.4.1: los importadores de GitHub/Bitbucket omiten el pinning de resolved_address, permitiendo que un usuario autenticado reasigne el DNS y acceda a hosts internos.
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · gitlab-gitaly-fetch-ssrf
GitLab CE 19.4.1 - GitLab Inc.
La importación de GitHub Enterprise / Bitbucket Server (y Direct Transfer, cuando ese flag está activado) omiten Projects::ImportService#get_resolved_address. Repository#fetch_as_mirror usa por defecto resolved_address: "". Gitaly FetchRemote / CreateRepositoryFromURL resuelve de nuevo el hostname del clon. UrlBlocker ya había permitido un registro A público. Rebind, y los objetos git-HTTP aterrizan en el proyecto importado.
Un usuario autenticado que puede ejecutar la importación de GitHub Enterprise puede hacer que Gitaly haga git-fetch a un host que ya no es la IP que UrlBlocker fijó.
| ID | sin CVE todavía |
| 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 |
| Producto | GitLab CE |
| Afectado | 19.4.1-ce.0 (26212baacadb en el árbol v19.4.1-ee) |
| Auth | usuario autenticado, importador de GitHub Enterprise / Bitbucket Server |
| Licencia | GNU Affero GPL v3.0 |
| Lab | solo 127.0.0.1 |
Apuntar la importación de GitHub Enterprise a un hostname que controla. Rails UrlBlocker.validate! permite un registro A público. ParallelImporter.imports_repository? es true, así que ImportService nunca fija. RepositoryImporter#fetch_as_mirror llama a Gitaly con un resolved_address vacío. Gitaly hace HTTP-fetch con git a lo que ese nombre sea ahora. El repo importado contiene los objetos.
En omnibus todo-en-uno eso es el loopback del worker. Los hostnames de clonado directo http://127.0.0.1 fallan en cerrado. La importación de repositorio por URL ya fija. Direct Transfer necesita bulk_import_enabled (por defecto false en self-managed).
Mismo producto, restos hermanos: Gitea HTTPS import pin drop, webhook 0.0.0.0/8.
Leí las notas del parche 19.4.1, luego los importadores. Projects::ImportService#get_resolved_address reescribe el hostname a la IP validada y lo pasa a Gitaly. Los importadores paralelos de GitHub y Bitbucket Server establecen imports_repository? y omiten esa ruta.
project.repository.fetch_as_mirror(project.unsafe_import_url, refmap: refmap, forced: true)
fetch_as_mirror usa por defecto resolved_address: "". El repository_pipeline.rb de Direct Transfer incluso llama a validate! y descarta el valor de retorno.
Levanté el stock gitlab/gitlab-ce:19.4.1-ce.0. En CE import_sources empieza vacío; el lab habilita GitHub. POST /api/v4/import/github con github_hostname=http://ghe.lab:18080. Catcher en el netns de GitLab habla JSON falso de GHE más git http-backend. El blob WITNESS del repo semilla es GITLAB-GITALY-FETCH-SSRF-WITNESS.
GET /api/v4/projects/1/repository/files/WITNESS/raw?ref=master devolvió el witness. Log de Gitaly: FetchRemote grpc.code=OK en root/ghe-ssrf. Catcher: git/2.55 info/refs y git-upload-pack. El negativo github_hostname=http://127.0.0.1:18080 es 400 Invalid URL.
Caminos equivocados ya registrados: la semilla CveLabRoot9! es un rechazo de WeakPasswords (substring root); 19.4 rellena más allá de esa comprobación a 64 caracteres; Faraday no puede conectar a una IP pública que no es el catcher, así que el harness REDIRECTa 1.1.1.1:18080 mientras UrlBlocker sigue viendo un A público; git-HTTP puede ganar antes de que GET /repos/org/repo cambie el DNS; GitLab da 502 a mitad de importación y luego el archivo está ahí de todos modos. Una reverse shell. Teatro. El oráculo es el blob importado más el FetchRemote de Gitaly.
cd lab
./run.sh
Objetivo solo http://127.0.0.1:18410. GitLab CE quiere varios GB de RAM. Solo 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)
Pasa get_resolved_address a fetch_as_mirror de la forma en que la importación de repositorio por URL ya lo hace. Tampoco descartes UrlBlocker.validate! en Direct Transfer.
repository_importer.rb · repository.rb · import_service.rbGNU Affero GPL v3.0. Ver LICENSE. Solo lab en loopback. Sin garantía.