
Drupal 11.4.8 - Alta - Remoto - SSRF - oEmbed discovery GETs an unscoped second-hop URL
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · drupal-oembed-ssrf
Classe: SSRF Alcance: Remoto
Drupal core 11.4.8 (61044508) - Drupal
SA-CORE-2026-008 restringiu o host do documento de descoberta com media_oembed_discovery_trusted_host_patterns. UrlResolver::findUrl ainda retorna o href bruto do link. ResourceFetcher::fetchResource então faz GET nessa URL sem verificação de host ou IP privado. O thumbnail_url do oEmbed é a mesma classe de GET. Os padrões padrão são vazios, então a descoberta fica desativada até que um admin a habilite.
Depois que um admin ativa a descoberta, um usuário que pode criar mídia oEmbed pode apontar o Drupal para uma página da allowlist cujo href do link oEmbed em JSON seja loopback, RFC1918 ou metadata.
| ID | sem CVE ainda |
| Classe | SSRF (resquício de segundo salto do SA-CORE-2026-008) |
| Alcance | Remoto (criação de mídia autenticada, após o admin definir os hosts de descoberta) |
| CWE | CWE-918 |
| CVSS | Médio: 6.1 CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:N/A:N |
| Produto | Drupal core |
| Afetado | 11.4.8 (61044508071749629aaf3380aa85971e94df9e84) |
| Auth | criação de mídia autenticada, após um admin definir os hosts de descoberta. AC:H é esse toggle do admin. |
| Licença | GNU Affero GPL v3.0 |
| Lab | apenas 127.0.0.1 |
Somente depois que um admin tiver definido os padrões de hosts confiáveis de descoberta. Um criador de mídia é suficiente depois disso. Um completo estranho sem conta não consegue fazer isso no padrão. Isto é SSRF, não RCE e não login de admin.
| Privilégio | Método / sink | Tipo | Comprovado em lab ou inferido |
|---|---|---|---|
| Fazer o Drupal dar GET em uma URL escolhida pelo atacante (loopback neste lab) | UrlResolver::findUrl com href bruto e depois GET de ResourceFetcher::fetchResource | recon (fetch do lado do servidor) | Comprovado em lab. http://127.0.0.1:18080/oembed retornou DRUPAL-OEMBED-SSRF-WITNESS |
| Ler o corpo HTTP dessa URL para o PHP | Mesmo GET; JSON/XML interpretado como oEmbed | recon | Comprovado em lab. O witness estava no corpo 200 |
Mesmo GET para thumbnail_url após um recurso ser construído | OEmbed::getLocalThumbnailUri ~417-456 | recon | Inferido a partir do pin. Este lab parou no href descoberto |
| Alcançar RFC1918 / link-local / metadata se o netns do PHP puder | GET do Guzzle sem escopo | recon | Inferido. O lab usou loopback, não 169.254.169.254 |
Fechado neste resquício: primeiro salto quando os padrões estão vazios (comprovado em lab); primeiro salto quando o host do HTML não corresponde (comprovado em lab); criação de mídia não autenticada no Standard sem um papel de mídia; RCE; roubo de sessão de admin.
Fiz o pin do Drupal 11.4.8 após o SA-CORE-2026-008. O primeiro salto é restringido:
$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 retorna o href bruto. ResourceFetcher faz GET nele com um timeout e nada mais:
$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,
]);
O lab é um gadget fiel dessas funções do 11.4.8, não uma instalação de site drupal:11.4.8 ao vivo. A última linha usa how=gadget. Padrões vazios não fizeram fetch. Um host não correspondente não fez fetch. Com ^firsthop$ permitido, discoverResourceUrl retornou http://127.0.0.1:18080/oembed e o GET sem escopo retornou o witness.
Caminhos errados já registrados: tratar o padrão desativado como um erro (é o gate pretendido do primeiro salto; o resquício é o salto dois); chamar uma instalação Standard ao vivo de oráculo (este pacote são as funções do pin); alegar RCE porque o Guzzle executou. Teatro: um reverse shell. O oráculo é padrão desativado mais host errado bloqueado mais o witness do segundo salto.
Mesmos resquícios do pin: CKEditor 5 FileIsImage skip.
python3 drupal-oembed-ssrf-Abraxas-Labs.py
ou
cd lab
./run.sh
Apenas loopback: HTML do primeiro salto em http://127.0.0.1:19510. O segundo salto é http://127.0.0.1:18080/oembed dentro do netns do 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
Aplique a mesma política de host / IP privado no endpoint descoberto e no thumbnail_url. Não faça fetch de um href cujo host teria falhado no primeiro salto.
61044508071749629aaf3380aa85971e94df9e84)UrlResolver.php · ResourceFetcher.phpGNU Affero GPL v3.0. Veja LICENSE. Apenas lab em loopback. Sem garantia.