
Drupal 11.4.8 — высокий уровень — удалённо — SSRF — oEmbed discovery выполняет GET-запрос по неограниченному URL второго перехода
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · drupal-oembed-ssrf
Разбор: https://abraxaslabs.tech/research/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 в этой лаборатории) | необработанный href из UrlResolver::findUrl, затем 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 без роли медиа; 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 внутри netns гаджета.
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. Без гарантий.