
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은 discovery document 호스트를 media_oembed_discovery_trusted_host_patterns로 제한했습니다. 그러나 UrlResolver::findUrl은 여전히 원시 link href를 반환합니다. 이어서 ResourceFetcher::fetchResource는 호스트나 사설 IP 검사 없이 해당 URL에 GET 요청을 보냅니다. oEmbed thumbnail_url도 같은 종류의 GET입니다. 기본 패턴은 비어 있으므로 관리자가 활성화하기 전까지 discovery는 꺼져 있습니다.
관리자가 discovery를 켠 후, oEmbed 미디어를 생성할 수 있는 사용자는 JSON oEmbed link href가 루프백, RFC1918 또는 메타데이터를 가리키는 허용 목록 페이지로 Drupal을 유도할 수 있습니다.
| ID | 아직 CVE 없음 |
| 분류 | SSRF (SA-CORE-2026-008의 2차 홉 잔여 문제) |
| 도달 범위 | 원격 (관리자가 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 core |
| 영향 받음 | 11.4.8 (61044508071749629aaf3380aa85971e94df9e84) |
| 인증 | 관리자가 discovery 호스트를 설정한 후 인증된 미디어 생성. AC:H는 해당 관리자 토글을 의미합니다. |
| 라이선스 | GNU Affero GPL v3.0 |
| 랩 | 127.0.0.1 전용 |
관리자가 discovery 신뢰 호스트 패턴을 설정한 경우에만 해당합니다. 그 이후에는 미디어 생성자면 충분합니다. 계정이 없는 완전한 외부인은 기본 설정에서 이를 수행할 수 없습니다. 이는 SSRF이며 RCE도, 관리자 로그인도 아닙니다.
| 권한 | 메서드 / 싱크 | 종류 | 랩 검증 또는 추론 |
|---|---|---|---|
| Drupal이 공격자가 선택한 URL(이 랩에서는 루프백)에 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 / 링크 로컬 / 메타데이터에 도달 | 범위가 지정되지 않은 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
루프백 전용: 첫 번째 홉 HTML은 http://127.0.0.1:19510에 있습니다. 두 번째 홉은 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를 참조하십시오. 루프백 실습 전용. 보증 없음.