
Drupal 11.4.8 — высокий уровень — удалённо — SSRF — oEmbed discovery выполняет GET-запрос по неограниченному URL второго перехода
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · drupal-oembed-ssrf
Класс: SSRF Доступ: Удалённый
Ядро Drupal 11.4.8 (61044508) - Drupal
SA-CORE-2026-008 ограничил хост discovery-документа с помощью media_oembed_discovery_trusted_host_patterns. UrlResolver::findUrl по-прежнему возвращает необработанный href из link. Затем ResourceFetcher::fetchResource выполняет GET этого URL без проверки хоста или приватного IP. oEmbed thumbnail_url — это тот же класс GET-запроса. По умолчанию шаблоны пусты, поэтому discovery отключён, пока администратор его не включит.
После того как администратор включит discovery, пользователь, имеющий право создавать oEmbed-медиа, может направить Drupal на страницу из списка разрешённых, чей JSON oEmbed link href указывает на loopback, RFC1918 или metadata.
| ID | CVE пока нет |
| Класс | SSRF (остаток второго перехода от SA-CORE-2026-008) |
| Доступ | Удалённый (аутентифицированное создание медиа, после того как администратор задаст хосты discovery) |
| CWE | CWE-918 |
| CVSS | Средний: 6.1 CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:N/A:N |
| Продукт | Ядро Drupal |
| Затронуто | 11.4.8 (61044508071749629aaf3380aa85971e94df9e84) |
| Аутентификация | аутентифицированное создание медиа, после того как администратор задаст хосты discovery. AC:H — это тот самый переключатель администратора. |
| Лицензия | GNU Affero GPL v3.0 |
| Лаборатория | только 127.0.0.1 |
Только после того как администратор задал доверенные шаблоны хостов discovery. После этого достаточно прав на создание медиа. Полностью посторонний без учётной записи не может сделать это при настройках по умолчанию. Это SSRF, а не RCE и не вход администратора.
| Привилегия | Метод / точка срабатывания | Тип | Доказано в лаборатории или предположено |
|---|---|---|---|
| Заставить Drupal выполнить GET на выбранный атакующим URL (в этой лаборатории loopback) | UrlResolver::findUrl необработанный href, затем GET в ResourceFetcher::fetchResource | разведка (серверный запрос) | Доказано в лаборатории. http://127.0.0.1:18080/oembed вернул DRUPAL-OEMBED-SSRF-WITNESS |
| Прочитать HTTP-тело этого URL в PHP | Тот же GET; JSON/XML парсится как oEmbed | разведка | Доказано в лаборатории. Свидетель был в теле ответа 200 |
Тот же GET для thumbnail_url после построения ресурса | OEmbed::getLocalThumbnailUri ~417-456 | разведка | Предположено на основе закреплённой версии. Эта лаборатория остановилась на обнаруженном href |
| Достичь RFC1918 / link-local / metadata, если это позволяет сетевой namespace PHP | Неограниченный GET через Guzzle | разведка | Предположено. В лаборатории использовался loopback, а не 169.254.169.254 |
Закрыто в этом остатке: первый переход, когда шаблоны пусты (доказано в лаборатории); первый переход, когда хост HTML не совпадает (доказано в лаборатории); неаутентифицированное создание медиа в Standard без роли media; RCE; кража сессии администратора.
Я закрепил Drupal 11.4.8 после SA-CORE-2026-008. Первый переход ограничен:
$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 возвращает необработанный href. ResourceFetcher выполняет GET по нему с таймаутом и больше ни с чем:
$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,
]);
Лаборатория — это точная имитация этих функций из 11.4.8, а не установка живого сайта drupal:11.4.8. Последняя строка использует how=gadget. Пустые шаблоны не привели к запросу. Несовпадающий хост не привёл к запросу. При разрешённом ^firsthop$ discoverResourceUrl вернул http://127.0.0.1:18080/oembed, и неограниченный GET вернул свидетеля.
Уже зафиксированные ошибочные пути: считать отключённость по умолчанию промахом (это и есть задуманное ограничение первого перехода; остаток — это второй переход); называть живую установку Standard оракулом (этот пакет — это закреплённые функции); заявлять о RCE из-за того, что выполнился Guzzle. Театр: обратная оболочка. Оракул — это отключённость по умолчанию плюс блокировка неверного хоста плюс свидетель второго перехода.
Другие остатки той же закреплённой версии: CKEditor 5 FileIsImage skip.
python3 drupal-oembed-ssrf-Abraxas-Labs.py
или
cd lab
./run.sh
Только loopback: HTML первого перехода на http://127.0.0.1:19510. Второй переход — http://127.0.0.1:18080/oembed внутри сетевого namespace гаджета.
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
Применяйте ту же политику хостов / приватных IP к обнаруженной конечной точке и к thumbnail_url. Не выполняйте запрос по href, чей хост не прошёл бы первый переход.
61044508071749629aaf3380aa85971e94df9e84)UrlResolver.php · ResourceFetcher.phpGNU Affero GPL v3.0. См. LICENSE. Только лаборатория на loopback. Без гарантий.