Nginx SSRF : détourner un proxy_pass concaténé pour atteindre un service interne
Une SSRF (Server-Side Request Forgery) consiste à faire émettre au serveur une requête vers une destination qu’il choisit à notre place. Sur Nginx, une directive proxy_pass qui insère une partie de l’URL cliente dans le nom d’hôte cible offre exactement ce levier : en manipulant ce fragment, on redirige la requête interne vers la ressource de son choix.
La configuration vulnérable
Section intitulée « La configuration vulnérable »location /uploads/ { allow 127.0.0.1; deny all; autoindex on; alias /var/www/app/uploads/;}
location ~ /dir_enum(.*) { proxy_pass http://internal-apache$1; proxy_redirect off;}Deux blocs se répondent.
/uploads/ est verrouillé. Seules les requêtes venant de 127.0.0.1 sont autorisées, tout le reste est bloqué par deny all. Mais autoindex on est actif : une requête qui atteindrait ce bloc depuis l’intérieur obtiendrait le listing complet des fichiers.
/dir_enum est le vecteur SSRF. Le bloc capture tout ce qui suit /dir_enum dans $1 et le concatène au nom d’hôte : proxy_pass http://internal-apache$1. Le fragment $1, entièrement contrôlé par le client, se retrouve collé au nom d’hôte de destination. C’est le point faible.
L’objectif : atteindre /uploads/ comme si la requête venait de 127.0.0.1, en passant par la SSRF de /dir_enum.
Rappel sur la structure d’une URL HTTP
Section intitulée « Rappel sur la structure d’une URL HTTP »Une URL HTTP suit la forme générale suivante :
http://username:password@hostname:port/pathusername:password: informations d’authentification HTTP, optionnelles.hostname: le serveur auquel se connecter.port: optionnel, 80 par défaut en HTTP.path: la ressource à récupérer.
L’élément décisif est le @. Tout ce qui le précède est traité comme du userinfo, tout ce qui le suit devient le véritable hôte. En injectant un @ dans $1, on transforme internal-apache en simple nom d’utilisateur et on impose notre propre destination.
Détourner la destination
Section intitulée « Détourner la destination »Le fragment injecté @127.0.0.1:80/uploads/ donne le proxy_pass suivant :
http://internal-apache@127.0.0.1:80/uploads/internal-apache devient le userinfo, la requête part vers 127.0.0.1:80/uploads/. Comme elle est désormais émise localement, le allow 127.0.0.1 du bloc /uploads/ est satisfait.
Première tentative :
http://exemple.fr/dir_enum@127.0.0.1:80/uploads/Résultat : 502 Bad Gateway. Le proxy n’a pas réussi à joindre l’hôte sous cette forme, l’adresse IP brute placée après le userinfo n’étant pas résolue comme attendu dans ce contexte.
Contourner avec nip.io
Section intitulée « Contourner avec nip.io »nip.io fournit un service DNS qui renvoie l’adresse IP encodée dans le nom de domaine. app.127.0.0.1.nip.io résout vers 127.0.0.1, sans configuration. Cela donne à Nginx un nom d’hôte résolvable qui pointe malgré tout sur la boucle locale.
http://exemple.fr/dir_enum@app.127.0.0.1.nip.io/uploads/Le proxy_pass effectif devient :
http://internal-apache@app.127.0.0.1.nip.io/uploads/L’hôte réel est app.127.0.0.1.nip.io, qui résout vers 127.0.0.1. La requête atteint le bloc /uploads/ depuis la boucle locale, autoindex renvoie le listing, et les fichiers restreints au réseau interne deviennent accessibles. sslip.io rend le même service et sert de solution de repli si nip.io est filtré.
Remédiation
Section intitulée « Remédiation »Ne pas concaténer d’entrée utilisateur dans proxy_pass
Section intitulée « Ne pas concaténer d’entrée utilisateur dans proxy_pass »La faille naît de $1 collé au nom d’hôte. La cible d’un proxy_pass doit être fixe, jamais construite à partir de l’URL cliente.
# Vulnérable : le client contrôle une partie de l'hôtelocation ~ /dir_enum(.*) { proxy_pass http://internal-apache$1;}
# Correct : hôte fixe, seul le chemin est transmis de façon contrôléelocation /dir_enum/ { proxy_pass http://internal-apache/;}Avec un location préfixe et un proxy_pass terminé par /, Nginx substitue proprement le chemin sans laisser le client réécrire l’hôte.
Valider l’hôte de destination
Section intitulée « Valider l’hôte de destination »Si une cible dynamique est réellement nécessaire, restreindre les destinations à une liste blanche via une map, et refuser tout ce qui n’y figure pas plutôt que de proxifier une valeur arbitraire.
Isoler par le réseau plutôt que par l’adresse source
Section intitulée « Isoler par le réseau plutôt que par l’adresse source »Un contrôle allow 127.0.0.1 tombe dès qu’une SSRF fait émettre la requête localement. Les ressources sensibles doivent être protégées par une authentification, pas seulement par l’adresse d’origine.
À retenir
Section intitulée « À retenir »Un proxy_pass qui intègre une portion d’URL cliente dans son nom d’hôte est une SSRF prête à l’emploi. L’injection d’un @ requalifie l’hôte prévu en userinfo et laisse choisir la vraie destination ; un service comme nip.io fournit au passage un nom résolvable pointant sur la boucle locale pour franchir les filtres basés sur l’adresse source. La cible d’un proxy ne doit jamais dépendre de l’entrée utilisateur, et une ressource interne se protège par authentification, pas par une simple restriction d’IP source.