Vaultwarden 1.37.3 : le contournement trusted-local par défaut de X-Real-IP permet de contourner les limites de tentatives de connexion sur le NAT Docker
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · vaultwarden-ip-header-spoof
Vaultwarden 1.37.3 - dani-garcia
La configuration par défaut IP_HEADER=X-Real-IP combinée à IP_HEADER_TRUSTED_PROXIES=local traite tout pair TCP non global comme un reverse proxy. Le NAT de port publié de Docker standard est en RFC1918. Un X-Real-IP fourni par le client devient la clé de limitation de débit. Les valeurs uniques ne partagent jamais une rafale.
| ID | pas encore de CVE |
| CWE | CWE-307 / CWE-290 |
| CVSS | Élevé : 7.5 CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N |
| Produit | Vaultwarden |
| Affecté | 1.37.3 (eb212e23). Image vaultwarden/server:1.37.3. Port publié Docker / userland-proxy / NAT Desktop, ou tout pair RFC1918. |
| Auth | non authentifié |
| Licence | GNU Affero GPL v3.0 |
| Lab | 127.0.0.1 uniquement |
L'attaquant supprime la protection anti-force brute basée sur l'IP. Il envoie un nouveau X-Real-IP à chaque tentative. Le régulateur est une DashMap indexée sur cette adresse, donc la rafale et la période ne se déclenchent jamais.
POST /identity/connect/token en password-grant. La rafale par défaut est de 10 par 60 secondes par IP. Des en-têtes uniques réinitialisent le compteur./api/two-factor/send-email-login, POST /admin, ainsi que les indices non authentifiés / prelogin / delete-recover / auth-request / accès Send sont tous indexés sur ClientIp.docker run -p 80:80 standard et compose 80:80 font du pair une adresse de bridge ou de NAT Desktop. trusted=local correspond à !is_global(peer). Cela correspond.Un reverse proxy qui remplace X-Real-IP par le vrai client, et une liaison hôte dont le pair est une adresse globale, maintiennent tous deux le régulateur honnête. La faille est la configuration par défaut plus un port publié sans écrasement.
J'ai épinglé dani-garcia/vaultwarden 1.37.3 (eb212e23) après les GHSAs 1.35.x / 1.36.0 / 1.37.0. GHSA-c5rv a placé send-email-login sur LIMITER_LOGIN. Le reste concerne la façon dont ClientIp est choisi.
config.rs définit par défaut ip_header à X-Real-IP et ip_header_trusted_proxies à local. auth.rs ip_header_is_trusted traite local comme !is_global(remote). ratelimit.rs indexe LIMITER_LOGIN sur cet IpAddr. Le bridge Docker, le NAT de la VM Desktop et le loopback sont tous non globaux.
J'ai déployé l'image standard vaultwarden/server:1.37.3 sur le loopback 127.0.0.1:18170:80 sans proxy d'écrasement, plus une seconde instance sur 18171 avec IP_HEADER=none. J'ai enregistré [email protected]. Le password-grant avec un mauvais mot de passe et sans en-tête a renvoyé 429 à la requête 11 (rafale 10). Le même grant avec X-Real-IP: 198.51.100.{n} unique à chaque tentative est resté en HTTP 400 quatorze fois, zéro 429. Sur l'instance none, des 203.0.113.{n} uniques ont quand même renvoyé 429 à la 11e.
SUCCESS VAULTWARDEN-IP-HEADER-SPOOF nohdr-429-at=11 spoof-400=14 spoof-429=0 none-429-at=11 VAULTWARDEN-IP-HEADER-SPOOF-WITNESS
Fausses pistes déjà consignées : l'image 1.37.3 n'était pas locale, donc le premier compose a attendu un pull ; le premier /alive a été une réponse vide pendant que Rocket se liait, puis 200 à l'attente 2 ; un JSON d'enregistrement factice (masterPasswordHash + une key courte) a suffi, pas de nouvelle tentative avec une clé longue. Un reverse shell. Du théâtre. L'oracle est 429 sans l'en-tête et 400 avec un en-tête unique.
cd lab
./run.sh
Cible uniquement http://127.0.0.1:18170 (par défaut) et http://127.0.0.1:18171 (IP_HEADER=none). Image vaultwarden/server:1.37.3.
Définir IP_HEADER par défaut à none, ou définir IP_HEADER_TRUSTED_PROXIES par défaut à une liste vide / un CIDR de proxy explicite. local comme « tout pair RFC1918 » est le cas du port publié. L'existence d'un en-tête de forwarding n'est pas une preuve de proxy.
src/config.rs (ip_header, ip_header_trusted_proxies)src/auth.rs (ip_header_is_trusted, ClientIp)src/ratelimit.rs