
Drupal 11.4.8 - Élevée - À distance - SSRF - La découverte oEmbed effectue un GET sur une URL de second saut non limitée
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · drupal-oembed-ssrf
Classe : SSRF Portée : Distante
Drupal core 11.4.8 (61044508) - Drupal
SA-CORE-2026-008 a protégé l'hôte du document de découverte avec media_oembed_discovery_trusted_host_patterns. UrlResolver::findUrl renvoie toujours le href brut du link. ResourceFetcher::fetchResource effectue ensuite un GET sur cette URL sans aucune vérification d'hôte ni d'IP privée. Le thumbnail_url oEmbed relève de la même catégorie de GET. Les motifs par défaut sont vides, donc la découverte est désactivée tant qu'un administrateur ne l'active pas.
Après qu'un administrateur a activé la découverte, un utilisateur pouvant créer des médias oEmbed peut pointer Drupal vers une page autorisée dont le href du lien oEmbed JSON est loopback, RFC1918 ou metadata.
| ID | pas encore de CVE |
| Classe | SSRF (reliquat du second saut de SA-CORE-2026-008) |
| Portée | Distante (création de média authentifiée, après que l'admin a défini les hôtes de découverte) |
| CWE | CWE-918 |
| CVSS | Moyen : 6.1 CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:N/A:N |
| Produit | Drupal core |
| Affecté | 11.4.8 (61044508071749629aaf3380aa85971e94df9e84) |
| Auth | création de média authentifiée, après qu'un admin a défini les hôtes de découverte. AC:H correspond à ce basculement admin. |
| Licence | GNU Affero GPL v3.0 |
| Lab | 127.0.0.1 uniquement |
Uniquement après qu'un administrateur a défini les motifs d'hôtes de confiance pour la découverte. Un créateur de média suffit ensuite. Un parfait inconnu sans compte ne peut pas faire cela par défaut. Il s'agit d'une SSRF, pas d'une RCE ni d'un vol de session admin.
| Privilège | Méthode / sink | Type | Prouvé en lab ou déduit |
|---|---|---|---|
| Faire effectuer à Drupal un GET vers une URL choisie par l'attaquant (loopback dans ce lab) | UrlResolver::findUrl href brut puis GET ResourceFetcher::fetchResource | reconnaissance (requête côté serveur) | Prouvé en lab. http://127.0.0.1:18080/oembed a renvoyé DRUPAL-OEMBED-SSRF-WITNESS |
| Lire le corps HTTP de cette URL dans PHP | Même GET ; JSON/XML analysé comme oEmbed | reconnaissance | Prouvé en lab. Le témoin était dans le corps de la réponse 200 |
Même GET pour thumbnail_url après qu'une ressource est construite | OEmbed::getLocalThumbnailUri ~417-456 | reconnaissance | Déduit du pin. Ce lab s'est arrêté au href découvert |
| Atteindre RFC1918 / link-local / metadata si le netns PHP le peut | GET Guzzle non restreint | reconnaissance | Déduit. Le lab a utilisé le loopback, pas 169.254.169.254 |
Fermé sur ce reliquat : premier saut lorsque les motifs sont vides (prouvé en lab) ; premier saut lorsque l'hôte HTML ne correspond pas (prouvé en lab) ; création de média non authentifiée sur Standard sans rôle média ; RCE ; vol de session admin.
J'ai épinglé Drupal 11.4.8 après SA-CORE-2026-008. Le premier saut est protégé :
$patterns = \array_map(static fn ($hostPattern) => \sprintf('{%s}i', $hostPattern), Settings::get('media_oembed_discovery_trusted_host_patterns', []));
// ...
if (!$allowed) {
return FALSE;
}
$response = $this->httpClient->get($url);
return $this->findUrl($xpath, 'json') ?: $this->findUrl($xpath, 'xml');
findUrl renvoie le href brut. ResourceFetcher effectue un GET dessus avec un timeout et rien d'autre :
$result = $xpath->query("//link[@type='application/$format+oembed']");
return $result->length ? $result->item(0)->getAttribute('href') : FALSE;
$response = $this->httpClient->request('GET', $url, [
RequestOptions::TIMEOUT => $this->timeout,
]);
Le lab est un gadget fidèle de ces fonctions de la 11.4.8, pas une installation live de drupal:11.4.8. La dernière ligne utilise how=gadget. Des motifs vides n'ont pas déclenché de requête. Un hôte non correspondant n'a pas déclenché de requête. Avec ^firsthop$ autorisé, discoverResourceUrl a renvoyé http://127.0.0.1:18080/oembed et le GET non restreint a renvoyé le témoin.
Fausses pistes déjà consignées : considérer le désactivé-par-défaut comme un échec (c'est la protection voulue du premier saut ; le reliquat est le saut deux) ; prendre une installation Standard live comme oracle (ce pack correspond aux fonctions épinglées) ; affirmer une RCE parce que Guzzle s'est exécuté. Théâtre : un reverse shell. L'oracle est désactivé-par-défaut plus hôte incorrect bloqué plus le témoin du second saut.
Autres reliquats du même pin : CKEditor 5 FileIsImage skip.
python3 drupal-oembed-ssrf-Abraxas-Labs.py
ou
cd lab
./run.sh
Loopback uniquement : HTML du premier saut sur http://127.0.0.1:19510. Le second saut est http://127.0.0.1:18080/oembed à l'intérieur du netns du gadget.
SUCCESS drupal-oembed-ssrf how=gadget pin=11.4.8 first-hop=allowed second-hop=127.0.0.1:18080 default-off=blocked DRUPAL-OEMBED-SSRF-WITNESS
Appliquez la même politique d'hôte / d'IP privée sur le point de terminaison découvert et sur thumbnail_url. Ne récupérez pas un href dont l'hôte aurait échoué au premier saut.
61044508071749629aaf3380aa85971e94df9e84)UrlResolver.php · ResourceFetcher.phpGNU Affero GPL v3.0. Voir LICENSE. Lab en loopback uniquement. Aucune garantie.