
Drupal 11.4.8 - 高 - リモート - SSRF - oEmbedディスカバリーがスコープ外のセカンドホップURLをGETする
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · drupal-oembed-ssrf
解説記事: https://abraxaslabs.tech/research/drupal-oembed-ssrf
クラス: SSRF 到達範囲: リモート
Drupal core 11.4.8 (61044508) - Drupal
SA-CORE-2026-008 は、ディスカバリドキュメントのホストを media_oembed_discovery_trusted_host_patterns で制限した。UrlResolver::findUrl は依然として生の link の href を返す。続いて ResourceFetcher::fetchResource がその URL をホストやプライベート IP のチェックなしに GET する。oEmbed の thumbnail_url も同種の GET である。デフォルトのパターンは空であるため、管理者が有効化するまでディスカバリは無効である。
管理者がディスカバリを有効化した後、oEmbed メディアを作成できるユーザーは、JSON oEmbed の link href がループバック、RFC1918、またはメタデータを指す許可リスト内のページへ Drupal を誘導できる。
| ID | CVE 未割当 |
| クラス | SSRF (SA-CORE-2026-008 のセカンドホップの取り残し) |
| 到達範囲 | リモート (管理者がディスカバリホストを設定した後の、認証済みメディア作成) |
| CWE | CWE-918 |
| CVSS | Medium: 6.1 CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:N/A:N |
| 製品 | Drupal core |
| 影響を受けるバージョン | 11.4.8 (61044508071749629aaf3380aa85971e94df9e84) |
| 認証 | 管理者がディスカバリホストを設定した後の、認証済みメディア作成。AC:H はこの管理者による切り替えを指す。 |
| ライセンス | GNU Affero GPL v3.0 |
| ラボ | 127.0.0.1 のみ |
管理者がディスカバリの信頼済みホストパターンを設定した後に限られる。その後はメディア作成者であれば十分である。アカウントを持たない完全な部外者はデフォルトではこれを実行できない。これは SSRF であり、RCE でも管理者ログインでもない。
| 権限 | メソッド / シンク | 種類 | ラボ実証または推測 |
|---|---|---|---|
| Drupal に攻撃者が選んだ URL (このラボではループバック) を GET させる | UrlResolver::findUrl の生の href、続いて ResourceFetcher::fetchResource の GET | 偵察 (サーバーサイドフェッチ) | ラボ実証済み。http://127.0.0.1:18080/oembed が DRUPAL-OEMBED-SSRF-WITNESS を返した |
| その URL の HTTP ボディを PHP に読み込ませる | 同じ GET。JSON/XML が oEmbed としてパースされる | 偵察 | ラボ実証済み。Witness は 200 のボディ内にあった |
リソース構築後に thumbnail_url に対して同じ GET を行う | OEmbed::getLocalThumbnailUri 約 417-456 行 | 偵察 | ピン留めしたバージョンからの推測。このラボはディスカバリされた href で停止した |
| PHP の netns が到達可能であれば RFC1918 / リンクローカル / メタデータに到達する | スコープなしの Guzzle GET | 偵察 | 推測。ラボではループバックを使用し、169.254.169.254 は使用しなかった |
この取り残しに関して塞がれているもの: パターンが空の場合のファーストホップ (ラボ実証済み)、HTML ホストが一致しない場合のファーストホップ (ラボ実証済み)、メディアロールなしの Standard での未認証メディア作成、RCE、管理者セッションの窃取。
SA-CORE-2026-008 の後、Drupal 11.4.8 をピン留めした。ファーストホップは制限されている:
$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,
]);
このラボは、実際の drupal:11.4.8 サイトインストールではなく、これら 11.4.8 の関数を忠実に再現したガジェットである。最終行は how=gadget を使用する。空のパターンでは取得されなかった。一致しないホストでは取得されなかった。^firsthop$ を許可した状態で、discoverResourceUrl は http://127.0.0.1:18080/oembed を返し、スコープなしの GET がウィットネスを返した。
すでに記録済みの誤った道筋:デフォルトオフをミスとみなすこと(これは意図されたファーストホップのゲートであり、残りはセカンドホップである);稼働中の Standard インストールをオラクルと呼ぶこと(このパックはピン留めされた関数である);Guzzle が実行されたという理由で RCE を主張すること。見せかけ:リバースシェル。オラクルとは、デフォルトオフに加えて不正ホストのブロック、そしてセカンドホップのウィットネスである。
同じピン留めの残り:CKEditor 5 FileIsImage skip。
python3 drupal-oembed-ssrf-Abraxas-Labs.py
または
cd lab
./run.sh
ループバックのみ:最初のホップは http://127.0.0.1:19510 上の HTML。2番目のホップは gadget netns 内の http://127.0.0.1:18080/oembed。
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
検出されたエンドポイントと thumbnail_url の両方に、同じホスト / プライベートIPポリシーを適用してください。最初のホップで失敗するはずだったホストを持つ href を取得しないでください。
61044508071749629aaf3380aa85971e94df9e84)UrlResolver.php · ResourceFetcher.phpGNU Affero GPL v3.0。LICENSE を参照してください。ループバックラボ専用です。保証はありません。