
Proof-of-concept e lab per un SSRF tramite webhook di GitLab CE sfruttando l'intervallo this-network 0.0.0.0/8, che consente a un Maintainer di raggiungere il loopback e leggere le risposte dai log degli hook.
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · gitlab-webhook-0net-ssrf
GitLab CE 19.4.1 - GitLab Inc.
Gitlab::HTTP_V2::UrlBlocker blocca esattamente 0.0.0.0, :: e 127.0.0.0/8. Non blocca il resto di 0.0.0.0/8. Linux considera quell'intervallo come "questa rete" e consegnerà 0.0.0.1 allo stack locale. Un Maintainer di un progetto può creare un webhook su http://0.0.0.1:<port>/ mentre allow_local_requests_from_web_hooks_and_services è disattivato. GitLab memorizza 8 KB della risposta nei log degli hook.
Un Maintainer del proprio progetto può far eseguire a GitLab una POST verso il loopback tramite 0.0.0.1 e leggere il corpo dal log dell'hook.
| ID | nessuna CVE al momento |
| 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 |
| Prodotto | GitLab CE |
| Affetto | 19.4.1-ce.0 (26212baacadb sull'albero v19.4.1-ee) |
| Auth | Maintainer autenticato (admin_web_hook) |
| Licenza | GNU Affero GPL v3.0 |
| Lab | solo 127.0.0.1 |
POST /api/v4/projects/:id/hooks con url=http://0.0.0.1:<port>/. Attivare o testare. Leggere response_body dall'API degli eventi dell'hook. Su omnibus all-in-one, Sidekiq condivide il netns del container con Puma, Workhorse e gli exporter. Il Maintainer del proprio progetto non dovrebbe leggerli.
http://127.0.0.1/ e http://169.254.169.254/ restano bloccati. Non è IMDS tramite link-local. È il buco della this-network.
Stesso prodotto, residui affini: Gitea HTTPS import pin drop, Gitaly fetch omits resolved_address.
Ho letto url_blocker.rb dopo le correzioni SSRF della 19.4.1. validate_localhost è esattamente "::" e "0.0.0.0" più Socket.ip_address_list. validate_loopback è ipv4_loopback? (127.0.0.0/8) e loopback ipv6. Le spec coprono gli alias ottali/esadecimali di 127.0.0.1. Non menzionano mai 0.0.0.1.
Il webhook è il sink del corpo: WebHookService esegue la POST e memorizza 8 KB. Il Maintainer ha admin_web_hook.
Ho avviato l'immagine stock gitlab/gitlab-ce:19.4.1-ce.0. Local-requests disattivato. Catcher dentro il netns di GitLab su 0.0.0.1:18080. Creazione hook http://127.0.0.1:18080/ e http://169.254.169.254/ : 422 Invalid url given. Creazione hook http://0.0.0.1:18080/ : 201. Test push events : 201. response_body dell'evento hook contiene GITLAB-WEBHOOK-0NET-SSRF-WITNESS.
Vicoli ciechi già registrati: /users/sign_in con Accept: application/json è 404; la password di seed contenente root è WeakPasswords; aggiungere 0.0.0.1 a lo lo mette su Socket.ip_address_list quindi validate_localhost risponde 422 al "positivo"; IP_FREEBIND più ip route add local 0.0.0.1/32 dev lo esegue il bind senza entrare nella lista degli indirizzi; il $ nel compose della password richiede $$. Una reverse shell. Teatro. L'oracolo è 201 su 0.0.0.1, 422 su 127.0.0.1, witness nel log.
cd lab
./run.sh
Target solo http://127.0.0.1:18420. GitLab CE richiede diversi GB di RAM. Solo lab autorizzato.
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
Negare 0.0.0.0/8 (e ::ffff:0.0.0.0/104) allo stesso modo in cui viene negato 127.0.0.0/8.
url_blocker.rb · web_hook_service.rb · project_hooks.rbGNU Affero GPL v3.0. Vedi LICENSE. Solo lab loopback. Nessuna garanzia.