
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
Análise: https://abraxaslabs.tech/research/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 um GET nessa URL sem verificação de host ou de 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 administrador a habilite.
Depois que um administrador ativa a descoberta, um usuário que pode criar mídia oEmbed pode apontar o Drupal para uma página da lista de permissões cujo href do link oEmbed em JSON seja loopback, RFC1918 ou metadata.
| ID | ainda sem CVE |
| Classe | SSRF (resíduo do segundo salto do SA-CORE-2026-008) |
| Alcance | Remoto (criação de mídia autenticada, após o administrador 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) |
| Autenticação | criação de mídia autenticada, após um administrador definir os hosts de descoberta. AC:H é essa alternância do administrador. |
| Licença | GNU Affero GPL v3.0 |
| Lab | apenas 127.0.0.1 |
Somente depois que um administrador tiver definido os padrões de hosts confiáveis de descoberta. Depois disso, um criador de mídia já é suficiente. Um completo desconhecido sem conta não consegue fazer isso na configuração padrão. Isto é SSRF, não RCE e não login de administrador.
| Privilégio | Método / sink | Tipo | Comprovado em lab ou inferido |
|---|---|---|---|
| Fazer o Drupal executar um GET em uma URL escolhida pelo atacante (loopback neste lab) | UrlResolver::findUrl com href bruto e depois GET em 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 no PHP | Mesmo GET; JSON/XML interpretado como oEmbed | recon | Comprovado em lab. O witness estava no corpo da resposta 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 resíduo: primeiro salto quando os padrões estão vazios (comprovado em lab); primeiro salto quando o host HTML não corresponde (comprovado em lab); criação de mídia não autenticada no Standard sem uma função de mídia; RCE; roubo de sessão de administrador.
Fixe o Drupal 11.4.8 após o SA-CORE-2026-008. O primeiro salto está 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 um GET com 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 laboratório é um gadget fiel dessas funções 11.4.8, não uma instalação ativa de um site drupal:11.4.8. A última linha usa how=gadget. Padrões vazios não buscaram. Um host não correspondente não buscou. Com ^firsthop$ permitido, discoverResourceUrl retornou http://127.0.0.1:18080/oembed e o GET sem escopo retornou a testemunha.
Desvios já registrados: tratar o padrão desativado por omissão como uma falha (é o portão de primeiro salto pretendido; o remanescente é o segundo salto); chamar uma instalação Standard ativa de oráculo (este pacote são as funções de fixação); alegar RCE porque o Guzzle foi executado. Teatro: uma reverse shell. O oráculo é o padrão desativado por omissão mais o host errado bloqueado mais a testemunha de segundo salto.
Mesmos remanescentes de fixação: CKEditor 5 FileIsImage skip.
python3 drupal-oembed-ssrf-Abraxas-Labs.py
ou
cd lab
./run.sh
Apenas loopback: primeiro salto HTML 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 em thumbnail_url. Não busque um href cujo host teria falhado no primeiro salto.
61044508071749629aaf3380aa85971e94df9e84)UrlResolver.php · ResourceFetcher.phpGNU Affero GPL v3.0. Consulte LICENSE. Apenas laboratório loopback. Sem garantias.