
Drupal 11.4.8 - Alta - Remoto - SSRF - oEmbed discovery esegue GET su un URL di secondo hop non limitato
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · drupal-oembed-ssrf
Write-up: https://abraxaslabs.tech/research/drupal-oembed-ssrf
Classe: SSRF Portata: Remota
Drupal core 11.4.8 (61044508) - Drupal
SA-CORE-2026-008 ha vincolato l'host del documento di discovery con media_oembed_discovery_trusted_host_patterns. UrlResolver::findUrl restituisce comunque l'href grezzo del link. ResourceFetcher::fetchResource esegue poi una GET su quell'URL senza alcun controllo sull'host o sugli IP privati. Il thumbnail_url di oEmbed è la stessa classe di GET. I pattern predefiniti sono vuoti, quindi la discovery è disattivata finché un amministratore non la abilita.
Dopo che un amministratore attiva la discovery, un utente che può creare media oEmbed può far puntare Drupal verso una pagina in allowlist il cui href del link oEmbed JSON è loopback, RFC1918 o metadata.
| ID | nessuna CVE al momento |
| Classe | SSRF (residuo del secondo hop di SA-CORE-2026-008) |
| Portata | Remota (creazione media autenticata, dopo che l'amministratore imposta gli host di discovery) |
| CWE | CWE-918 |
| CVSS | Medio: 6.1 CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:N/A:N |
| Prodotto | Drupal core |
| Affetto | 11.4.8 (61044508071749629aaf3380aa85971e94df9e84) |
| Auth | creazione media autenticata, dopo che un amministratore imposta gli host di discovery. AC:H corrisponde a quel toggle dell'amministratore. |
| Licenza | GNU Affero GPL v3.0 |
| Lab | solo 127.0.0.1 |
Solo dopo che un amministratore ha impostato i pattern degli host fidati per la discovery. Dopo di che basta un creatore di media. Un completo estraneo senza account non può farlo con la configurazione predefinita. Si tratta di SSRF, non di RCE e non di un login da amministratore.
| Privilegio | Metodo / sink | Tipo | Dimostrato in lab o dedotto |
|---|---|---|---|
| Far eseguire a Drupal una GET su un URL scelto dall'attaccante (loopback in questo lab) | UrlResolver::findUrl con href grezzo, poi GET di ResourceFetcher::fetchResource | ricognizione (fetch lato server) | Dimostrato in lab. http://127.0.0.1:18080/oembed ha restituito DRUPAL-OEMBED-SSRF-WITNESS |
| Leggere il corpo HTTP di quell'URL in PHP | Stessa GET; JSON/XML interpretati come oEmbed | ricognizione | Dimostrato in lab. Il witness era nel corpo della risposta 200 |
Stessa GET per thumbnail_url dopo la costruzione di una risorsa | OEmbed::getLocalThumbnailUri ~417-456 | ricognizione | Dedotto dal pin. Questo lab si è fermato all'href scoperto |
| Raggiungere RFC1918 / link-local / metadata se il netns PHP lo consente | GET Guzzle senza restrizioni | ricognizione | Dedotto. Il lab ha usato il loopback, non 169.254.169.254 |
Chiusi su questo residuo: primo hop quando i pattern sono vuoti (dimostrato in lab); primo hop quando l'host HTML non corrisponde (dimostrato in lab); creazione media non autenticata su Standard senza un ruolo media; RCE; furto della sessione amministratore.
Ho fissato Drupal 11.4.8 dopo SA-CORE-2026-008. Il primo hop è vincolato:
$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 restituisce l'href grezzo. ResourceFetcher lo recupera con GET con un timeout e nient'altro:
$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,
]);
Il lab è un gadget fedele di quelle funzioni della 11.4.8, non un'installazione live di un sito drupal:11.4.8. L'ultima riga usa how=gadget. I pattern vuoti non hanno effettuato il fetch. Un host non corrispondente non ha effettuato il fetch. Con ^firsthop$ consentito, discoverResourceUrl ha restituito http://127.0.0.1:18080/oembed e la GET senza scope ha restituito il witness.
Vicoli ciechi già registrati: considerare il default-off come un mancato riscontro (è il gate intenzionale del primo hop; ciò che resta è il secondo hop); chiamare oracle un'installazione Standard live (questo pack è costituito dalle funzioni del pin); affermare l'RCE perché Guzzle è stato eseguito. Teatro: una reverse shell. L'oracle è default-off più host errato bloccato più il witness del secondo hop.
Stessi residui del pin: CKEditor 5 FileIsImage skip.
python3 drupal-oembed-ssrf-Abraxas-Labs.py
o
cd lab
./run.sh
Solo loopback: primo hop HTML su http://127.0.0.1:19510. Il secondo hop è http://127.0.0.1:18080/oembed all'interno del netns del 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
Applicare la stessa policy host / IP privato sull'endpoint scoperto e su thumbnail_url. Non recuperare un href il cui host avrebbe fallito il primo hop.
61044508071749629aaf3380aa85971e94df9e84)UrlResolver.php · ResourceFetcher.phpGNU Affero GPL v3.0. Vedi LICENSE. Solo per laboratorio loopback. Nessuna garanzia.