abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · drupal-oembed-ssrf
类别: SSRF 可达性: 远程
Drupal 核心 11.4.8 (61044508) - Drupal
SA-CORE-2026-008 使用 media_oembed_discovery_trusted_host_patterns 对发现文档主机进行了限制。UrlResolver::findUrl 仍然返回原始的 link href。ResourceFetcher::fetchResource 随后对该 URL 发起 GET 请求,且没有进行主机或私有 IP 检查。oEmbed 的 thumbnail_url 属于同一类 GET 请求。默认模式为空,因此在管理员启用之前,发现功能处于关闭状态。
在管理员开启发现功能后,能够创建 oEmbed 媒体的用户可以引导 Drupal 访问一个允许列表中的页面,该页面的 JSON oEmbed 链接 href 指向回环地址、RFC1918 地址或元数据地址。
| ID | 暂无 CVE |
| 类别 | SSRF(SA-CORE-2026-008 的第二跳遗留问题) |
| 可达性 | 远程(在管理员设置发现主机后,经过身份验证的媒体创建) |
| 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) |
| 认证 | 经过身份验证的媒体创建,需管理员设置发现主机。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 | 侦察 | 实验验证。见证字符串出现在 200 响应体中 |
在构建资源后对 thumbnail_url 发起同样的 GET | OEmbed::getLocalThumbnailUri 约 417-456 行 | 侦察 | 根据固定版本推断。本实验止步于发现的 href |
| 如果 PHP 网络命名空间可达,则访问 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,
]);
本实验是对这些 11.4.8 函数的忠实复现装置,而非一个运行中的 drupal:11.4.8 站点安装。最后一行使用 how=gadget。空模式不会发起请求。不匹配的主机不会发起请求。在允许 ^firsthop$ 的情况下,discoverResourceUrl 返回了 http://127.0.0.1:18080/oembed,未限制范围的 GET 返回了见证字符串。
已记录的弯路:将默认关闭视为漏报(它是有意设计的第一跳限制;遗留问题在第二跳);将运行中的 Standard 安装当作判定依据(本包是固定版本的函数);因为 Guzzle 执行了就声称 RCE。表演性质的内容:反弹 shell。判定依据是默认关闭加上错误主机被阻止加上第二跳见证。
同一固定版本的其他遗留问题:CKEditor 5 FileIsImage 跳过。
python3 drupal-oembed-ssrf-Abraxas-Labs.py
或
cd lab
./run.sh
仅回环:第一跳 HTML 位于 http://127.0.0.1:19510。第二跳是装置网络命名空间内的 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。仅限回环实验。无担保。