
Drupal 11.4.8 - Alta - Remota - SSRF - El descubrimiento de oEmbed realiza GETs a una URL de segundo salto sin alcance
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · drupal-oembed-ssrf
Análisis: https://abraxaslabs.tech/research/drupal-oembed-ssrf
Clase: SSRF Alcance: Remoto
Drupal core 11.4.8 (61044508) - Drupal
SA-CORE-2026-008 restringió el host del documento de descubrimiento con media_oembed_discovery_trusted_host_patterns. UrlResolver::findUrl sigue devolviendo el href del link sin procesar. ResourceFetcher::fetchResource luego hace un GET a esa URL sin comprobar el host ni la IP privada. El thumbnail_url de oEmbed es la misma clase de GET. Los patrones predeterminados están vacíos, por lo que el descubrimiento está desactivado hasta que un administrador lo habilite.
Después de que un administrador active el descubrimiento, un usuario que pueda crear medios oEmbed puede apuntar Drupal a una página en la lista de permitidos cuyo href del enlace oEmbed en JSON sea loopback, RFC1918 o metadata.
| ID | aún sin CVE |
| Clase | SSRF (residuo del segundo salto de SA-CORE-2026-008) |
| Alcance | Remoto (creación de medios autenticada, después de que el administrador configure los hosts de descubrimiento) |
| 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 |
| Producto | Drupal core |
| Afectado | 11.4.8 (61044508071749629aaf3380aa85971e94df9e84) |
| Autenticación | creación de medios autenticada, después de que un administrador configure los hosts de descubrimiento. AC:H es ese interruptor del administrador. |
| Licencia | GNU Affero GPL v3.0 |
| Laboratorio | solo 127.0.0.1 |
Solo después de que un administrador haya configurado los patrones de hosts de confianza para el descubrimiento. Después de eso, basta con un creador de medios. Un completo desconocido sin cuenta no puede hacer esto con la configuración predeterminada. Esto es SSRF, no RCE ni un inicio de sesión de administrador.
| Privilegio | Método / sink | Tipo | Demostrado en laboratorio o inferido |
|---|---|---|---|
| Hacer que Drupal haga un GET a una URL elegida por el atacante (loopback en este laboratorio) | UrlResolver::findUrl con el href sin procesar y luego el GET de ResourceFetcher::fetchResource | reconocimiento (fetch del lado del servidor) | Demostrado en laboratorio. http://127.0.0.1:18080/oembed devolvió DRUPAL-OEMBED-SSRF-WITNESS |
| Leer el cuerpo HTTP de esa URL en PHP | El mismo GET; JSON/XML analizado como oEmbed | reconocimiento | Demostrado en laboratorio. El testigo estaba en el cuerpo 200 |
El mismo GET para thumbnail_url después de construir un recurso | OEmbed::getLocalThumbnailUri ~417-456 | reconocimiento | Inferido a partir de la versión fijada. Este laboratorio se detuvo en el href descubierto |
| Alcanzar RFC1918 / link-local / metadata si el netns de PHP puede | GET de Guzzle sin restricciones | reconocimiento | Inferido. El laboratorio usó loopback, no 169.254.169.254 |
Descartado en este residuo: primer salto cuando los patrones están vacíos (demostrado en laboratorio); primer salto cuando el host HTML no coincide (demostrado en laboratorio); creación de medios no autenticada en Standard sin un rol de medios; RCE; robo de sesión de administrador.
Fijé Drupal 11.4.8 después de SA-CORE-2026-008. El primer 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 devuelve el href sin procesar. ResourceFetcher lo obtiene mediante GET con un tiempo de espera y nada más:
$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,
]);
El laboratorio es un gadget fiel de esas funciones de la 11.4.8, no una instalación de sitio drupal:11.4.8 en vivo. La última línea usa how=gadget. Los patrones vacíos no hicieron fetch. Un host que no coincide no hizo fetch. Con ^firsthop$ permitido, discoverResourceUrl devolvió http://127.0.0.1:18080/oembed y el GET sin alcance devolvió el testigo.
Errores ya registrados: tratar el default-off como un fallo (es la puerta de primer salto prevista; lo sobrante es el segundo salto); llamar oráculo a una instalación Standard en vivo (este pack son las funciones fijadas); afirmar RCE porque Guzzle se ejecutó. Teatro: una reverse shell. El oráculo es default-off más host incorrecto bloqueado más el testigo de segundo salto.
Mismos restos fijados: CKEditor 5 FileIsImage skip.
python3 drupal-oembed-ssrf-Abraxas-Labs.py
o
cd lab
./run.sh
Solo loopback: el HTML del primer salto en http://127.0.0.1:19510. El segundo salto es http://127.0.0.1:18080/oembed dentro 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
Aplica la misma política de host / IP privada en el endpoint descubierto y en thumbnail_url. No obtengas un href cuyo host habría fallado en el primer salto.
61044508071749629aaf3380aa85971e94df9e84)UrlResolver.php · ResourceFetcher.phpGNU Affero GPL v3.0. Consulta LICENSE. Solo para laboratorio en loopback. Sin garantía.