Увага
Цей матеріал створено виключно в освітніх цілях та для підвищення обізнаності щодо інформаційної безпеки. Використання описаних технік проти систем без явного письмового дозволу власника є незаконним у більшості юрисдикцій.
У лютому 2026 року Ghost — одна з найпопулярніших Node.js-платформ для блогів та медіа-видань — отримала патч для критичної вразливості CVE-2026-26980. Вона дозволяє будь-якому неавторизованому атакуючому читати довільні дані з бази даних через публічний Content API. Ніякого логіна, ніякого токена — лише HTTP-запит із crafted payload. Ми вже показували живу демонстрацію цієї атаки у нашому відео — від пошуку вразливих інсталяцій через Shodan до витягу bcrypt-хешу адміна. А у цій статті розберемо, як саме працює ця вразливість під капотом, чому вона така небезпечна і як від неї захиститися.
Інфо
CVE-2026-26980 | Вразливі версії: Ghost 3.24.0 – 6.19.0 | Виправлено у: 6.19.1 | Тип атаки: Unauthenticated Blind SQLi (ORDER BY CASE) | Вектор: Content API, функція slugFilterOrder | СУБД: SQLite / MySQL
Масштаб проблеми
Щоб зрозуміти, наскільки серйозна ця вразливість, достатньо подивитися на Shodan. Пошук по HTTP-fingerprint Ghost CMS дає понад 7 600 публічно доступних інсталяцій — переважно у США, Польщі та Німеччині. І значна їх частина станом на момент написання статті ще не оновлена до 6.19.1. Ghost використовують від персональних блогерів до великих медіа-видань, тож потенційна поверхня атаки — величезна.
Де знаходиться вразливість
Content API Ghost доступний за шляхом /ghost/api/content/ і приймає публічний API-ключ. І тут перший нюанс: цей ключ не є секретом — він лежить прямо в HTML головної сторінки будь-якого Ghost-сайту в атрибуті data-key. Так і задумано, адже Content API призначений для публічного читання контенту.
Сама ж вразливість знаходиться у функції slugFilterOrder, яка обробляє параметр filter=slug:[...] для сортування результатів. Розробники хотіли дати можливість повертати пости/теги у тому порядку, в якому клієнт передав slug-и — і реалізували це через динамічну генерацію ORDER BY CASE виразу.
Механізм ін'єкції
Вразливий код у функції slugFilterOrder виглядав приблизно так:
javascript// Вразливий код (до патчу):
order += `WHEN \`${table}\`.\`slug\` = '${slug}' THEN ${index} `;Проблема очевидна: змінна slug береться безпосередньо з параметра фільтра без будь-якого екранування і вставляється у SQL-вираз через шаблонну літеру. Класична конкатенація, класичний фейл. Зловмисник може підставити у slug спеціально сформований рядок, який "виламує" його з контексту одинарних лапок і впроваджує довільний SQL.
Припустимо, зловмисник передає у filter slug виду ' OR (SELECT 1)=1 THEN (SELECT abs(-9223372036854775808)) WHEN slug='. Після конкатенації в серверному коді отриманий SQL-запит виглядатиме так:
sqlORDER BY CASE
WHEN `table`.`slug` = '' OR (SELECT 1)=1 THEN (SELECT abs(-9223372036854775808))
WHEN `table`.`slug` = 'normal-slug' THEN 1
END ASCЦе і є впровадження — ми опинилися всередині CASE-виразу з можливістю виконати довільні підзапити в WHEN-гілці.
Error-based blind SQLi через переповнення
Ghost не повертає результати підзапитів клієнту — Content API показує лише нормалізований JSON з постами/тегами. Тож пряма ексфільтрація через SELECT неможлива. Але є хитрощі: error-based blind SQLi.
Суть така: підзапит SELECT abs(-9223372036854775808) викликає integer overflow у SQLite (для MySQL використовується SELECT exp(710), що дає floating-point overflow). Коли БД зустрічає таке — вона кидає виняток, який проходить через весь Node.js стек і повертається як HTTP 500 з InternalServerError. Якщо ж умова хибна — помилки немає, виконується нормальне сортування, і ми отримуємо HTTP 200.
Ось так ми і отримуємо бінарний oracle: 500 = TRUE, 200 = FALSE. Замість (SELECT 1)=1 підставляємо реальні умови для витягу даних:
sql-- Витяг першого символу пароля адміністратора:
(SELECT SUBSTR(password,1,1) FROM users LIMIT 1) >= 'a'
-- Витяг довжини email адміністратора:
LENGTH((SELECT email FROM users LIMIT 1)) >= 20
-- Витяг Admin API secret:
(SELECT SUBSTR(secret,1,1) FROM api_keys WHERE type='admin' LIMIT 1) = 'f'Далі — діло техніки. Через бінарний пошук визначаємо довжину цільового поля (log₂(N) запитів), потім посимвольно витягуємо значення — знову ж таки через binary search по charset. При 15 паралельних потоках повний витяг bcrypt-хешу (60 символів) займає від 5 до 10 хвилин.
Загальний флоу атаки
- Виявлення — витягнути публічний API key з HTML головної сторінки (
data-key) - Калібрація oracle — тест базових умов
1=1і1=2, щоб переконатися що сервер дійсно повертає 500/200 як очікується - Вибір мети — визначення СУБД (SQLite чи MySQL) через характерні відмінності у синтаксисі
- Визначення довжин — бінарний пошук розмірів полів (email, password, secret)
- Посимвольний витяг — паралельний binary search по кожному символу
- Профіт — у атакуючого на руках email адміна, bcrypt-хеш пароля і, що найстрашніше, Admin API secret
Що можна витягнути і чому це страшно
Вразливість дає доступ до довільних таблиць у БД Ghost. Найцікавіші з точки зору атакуючого:
users— email адміністратора, ім'я, bcrypt-хеш пароля, статус акаунтуapi_keys— Admin API key та secret (це дає повний адмін-доступ без пароля)members— база підписників сайту (імена, email-адреси)sessions— активні сесії, потенційне session hijacking
І тут важливий нюанс: витяг Admin API secret критичніший за витяг хешу пароля. Хеш потрібно ще зламати (про це нижче), а з Admin API secret зловмисник миттєво генерує валідний JWT-токен і заходить в адмінку як будь-який користувач — без пароля, без 2FA, без нічого. Кінець історії.
Стійкість bcrypt: чому пароль все ще має значення
Ghost зберігає паролі у форматі bcrypt з cost factor $10. Це хороше рішення розробників — bcrypt спеціально спроектований повільним, щоб ускладнити брутфорс навіть після витоку хешу. Подивимося на структуру хешу:
plaintext$2a$10$SaltSaltSaltSaltSaltSaHashHashHashHashHashHashHashHashHash
$2a$ → алгоритм bcrypt
10 → cost factor (2^10 = 1024 ітерацій хешування)
[22 chars] → сіль (унікальна для кожного пароля)
[31 chars] → сам хешЧому це так суттєво? Сучасний GPU RTX 4090 перебирає bcrypt $10 зі швидкістю ~184 хешів/секунду. Для порівняння: MD5 той самий GPU молотить зі швидкістю кілька мільярдів хешів/секунду. Різниця — 10⁷ разів. Це перетворює задачу "зламати пароль за день" у задачу "зламати пароль за мільйони років" — якщо пароль нормальний.
Hashcat і аудит паролів
Hashcat — стандартний інструмент для аудиту паролів, який використовують пентестери та адміністратори для перевірки реальної стійкості хешів у своїх системах. Якщо ви адмін Ghost і хочете зрозуміти, чи витримає ваш пароль витік — ось як це перевірити. Для bcrypt використовується режим -m 3200:
bash# Перевірка стійкості власного хешу за словником:
$ hashcat -m 3200 -a 0 hash.txt rockyou.txt
# З правилами мутації (найкраще для реальних паролів):
$ hashcat -m 3200 -a 0 hash.txt rockyou.txt -r best64.ruleЯкщо ваш пароль знаходиться за кілька хвилин — його точно треба міняти. Якщо hashcat гризе його добу і не знаходить — ви у відносній безпеці навіть у разі витоку хешу. Висновок простий: довгий унікальний пароль з менеджера паролів + bcrypt = реальний бар'єр, а не просто уповільнювач.
Як виявити атаку в логах
Blind SQLi через ORDER BY CASE генерує дуже характерні патерни. Атака потребує тисяч запитів на один і той же endpoint з чергуванням 500/200 відповідей — це не пропустити, якщо знати що шукати:
bash# Індикатори компрометації у Nginx/Apache access logs:
# 1. Чергування 500/200 на /ghost/api/content/ з одного IP:
192.168.1.1 - - [21/Apr/2026] "GET /ghost/api/content/tags/?key=...&filter=slug:[...] 500
192.168.1.1 - - [21/Apr/2026] "GET /ghost/api/content/tags/?key=...&filter=slug:[...] 200
192.168.1.1 - - [21/Apr/2026] "GET /ghost/api/content/tags/?key=...&filter=slug:[...] 500
# 2. URL-encoded підозрілі символи в filter=slug:
# Шукати: %27 ('), OR, SELECT, abs(, SUBSTR, LENGTH
# 3. Сотні або тисячі запитів/хв з одного IP на Content API
# Швидкий grep для виявлення:
$ grep "content/" access.log | grep " 500 " | awk '{print $1}' | sort | uniq -c | sort -rn
$ grep -E "filter.*slug.*(OR|SELECT|abs|SUBSTR)" access.logЯк захиститися
Якщо ви адмініструєте Ghost-сайт — ось список дій у порядку пріоритету:
- Оновіть Ghost до 6.19.1 або вище — це єдиний надійний спосіб закрити вразливість. Патч додає proper SQL-екранування у функції
slugFilterOrder. - Змініть пароль і регенеруйте всі API-ключі — якщо ваш сайт був публічно доступний і працював на вразливій версії, вважайте credentials потенційно скомпрометованими. Це параноя, але виправдана.
- Увімкніть rate limiting на Content API — Blind SQLi фізично вимагає сотні/тисячі запитів. Nginx
limit_req_zoneдля/ghost/api/content/робить атаку непрактичною навіть на уразливих версіях. - WAF-правило на filter= параметр — блокуйте запити з SQL-ключовими словами (
OR,SELECT,abs(,SUBSTR) у параметріfilter=slug:через ModSecurity або Cloudflare WAF. - Увімкніть 2FA в Ghost Admin — навіть якщо хеш пароля витікає і зламається, 2FA блокує вхід. Це не захистить від витоку API secret, але захистить від pass-the-hash сценаріїв.
- Налаштуйте моніторинг — алерт на >50 запитів/хв на Content API з чергуванням 500/200 відповідями з одного IP. Це дешево і ловить навіть 0-day варіації цієї техніки.
Висновок
CVE-2026-26980 — повчальний приклад того, як навіть зрілі проєкти з мільйонами користувачів роками тягнуть базові помилки на кшталт SQL-конкатенації. Вразливість проста (буквально template literal замість параметризованого запиту), неавтентифікована, і її вплив — повна компрометація адмінки за кілька хвилин.
Хороша новина: патч вже є (Ghost 6.19.1), оновлення робиться однією командою ghost update, і якщо ви адмін — оновіться сьогодні. Не завтра, не "як буде вільний вечір". Сьогодні.
В нас такого багато!
Перейти


