
Drupal 11.4.8 - 높음 - 원격 - SSRF - oEmbed 검색이 범위가 지정되지 않은 두 번째 홉 URL을 GET함
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · drupal-oembed-ssrf
분류: SSRF 도달 범위: 원격
Drupal core 11.4.8 (61044508) - Drupal
SA-CORE-2026-008은 discovery document 호스트를 media_oembed_discovery_trusted_host_patterns로 제한했다. UrlResolver::findUrl은 여전히 원시 link href를 반환한다. 이어서 ResourceFetcher::fetchResource가 해당 URL을 호스트나 사설 IP 검사 없이 GET한다. oEmbed thumbnail_url도 같은 종류의 GET이다. 기본 패턴은 비어 있으므로 관리자가 활성화하기 전까지 discovery는 꺼져 있다.
관리자가 discovery를 켠 후, oEmbed 미디어를 생성할 수 있는 사용자는 JSON oEmbed link href가 loopback, RFC1918 또는 메타데이터인 allowlist 페이지를 Drupal이 가리키도록 할 수 있다.
| ID | 아직 CVE 없음 |
| 분류 | SSRF (SA-CORE-2026-008의 두 번째 홉 잔여물) |
| 도달 범위 | 원격 (관리자가 discovery 호스트를 설정한 후 인증된 미디어 생성) |
| 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) |
| 인증 | 관리자가 discovery 호스트를 설정한 후 인증된 미디어 생성. AC:H는 그 관리자 토글을 의미한다. |
| 라이선스 | GNU Affero GPL v3.0 |
| 랩 | 127.0.0.1 전용 |
관리자가 discovery trusted-host 패턴을 설정한 후에만 가능하다. 그 이후에는 미디어 생성자면 충분하다. 계정이 없는 완전한 외부인은 기본 설정에서 이를 할 수 없다. 이는 SSRF이며 RCE도, 관리자 로그인도 아니다.
| 권한 | 메서드 / 싱크 | 종류 | 랩 입증 또는 추론 |
|---|---|---|---|
| Drupal이 공격자가 선택한 URL(이 랩에서는 loopback)을 GET하도록 만들기 | UrlResolver::findUrl 원시 href 후 ResourceFetcher::fetchResource GET | 정찰 (서버 측 fetch) | 랩 입증. 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 / link-local / 메타데이터 도달 | 범위 미제한 Guzzle GET | 정찰 | 추론. 랩은 169.254.169.254가 아닌 loopback을 사용함 |
이 잔여물에서 닫힌 것: 패턴이 비어 있을 때의 첫 번째 홉(랩 입증); 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을 사용한다. 빈 패턴은 fetch하지 않았다. 일치하지 않는 호스트는 fetch하지 않았다. ^firsthop$가 허용된 상태에서 discoverResourceUrl은 http://127.0.0.1:18080/oembed를 반환했고 범위 미제한 GET이 witness를 반환했다.
이미 기록된 잘못된 방향: 기본 비활성화를 실패로 간주하기(이는 의도된 첫 번째 홉 게이트이며, 잔여물은 두 번째 홉이다); 실제 Standard 설치를 오라클로 부르기(이 팩은 핀 함수들이다); Guzzle이 실행되었다는 이유로 RCE를 주장하기. 연극: 리버스 셸. 오라클은 기본 비활성화 + 잘못된 호스트 차단 + 두 번째 홉 witness이다.
같은 핀의 잔여물: CKEditor 5 FileIsImage skip.
python3 drupal-oembed-ssrf-Abraxas-Labs.py
또는
cd lab
./run.sh
Loopback 전용: 첫 번째 홉 HTML은 http://127.0.0.1:19510에 있다. 두 번째 홉은 가젯 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는 fetch하지 말라.
61044508071749629aaf3380aa85971e94df9e84)UrlResolver.php · ResourceFetcher.phpGNU Affero GPL v3.0. LICENSE 참조. Loopback 랩 전용. 보증 없음.