
PoC для CVE-2026-72708 (слепая SQLi) и CVE-2026-109378 (SSRF) в SPIP, обе исправлены в 4.4.18
Воспроизводимые материалы proof of concept для двух уязвимостей, о которых я сообщил в SPIP, обе исправлены в 4.4.18.
| CVE | Класс | Аутентификация | CVSS 4.0 | Точка входа |
|---|---|---|---|---|
| CVE-2026-72708 | Слепая SQL-инъекция (CWE-89) | отсутствует | 8.7 High | ecrire/req/mysql.php:1730 |
| CVE-2026-109378 | SSRF через HTTP-редирект (CWE-918) | низкая | 5.3 Medium | ecrire/inc/distant.php:567-582 |
Обе были отправлены на [email protected] 2026-08-05. SPIP выпустил исправление в
4.4.18 2026-08-10. Эти материалы публикуются после патча, в чём и заключается весь смысл их публикации.
Всё здесь запускается против одноразовой локальной лаборатории, привязанной к
127.0.0.1. Не направляйте это на хост, которым вы не владеете.
spip_mysql_cite() возвращает значение без кавычек, когда целевой столбец имеет
тип даты и значение соответствует /^\w+\(/:
} elseif (sql_test_date($type) and preg_match('/^\w+\(/', $v)) {
return $v;
}
Регулярное выражение должно было пропускать NOW() как вызов функции, а не как строку. Оно проверяет форму вызова функции, а не её имя, поэтому SLEEP(5), IF(...) и UPDATEXML(...) также ему соответствуют.
Достижимый сток — это критерий {annee?} в squelettes-dist/sitemap.xml.html, который поставляется с каждой установкой и сравнивается со столбцом datetime. Карта сайта публична, поэтому учётная запись не требуется:
GET /spip.php?page=sitemap.xml&annee=IF(1,SLEEP(5),0)
Запустите его:
bash CVE-2026-72708-sqli/poc_sqli.sh
Скрипт подтверждает наличие уязвимости, демонстрирует задержку, зависящую от аргумента SLEEP(), затем восстанавливает первые двенадцать символов хеша пароля администратора через чистый временной оракул и сверяет результат с базой данных.
SLEEP() находится в предложении WHERE, которое MySQL вычисляет для каждой строки. При нулевом количестве статей он никогда не срабатывает. lab/seed.sh вставляет одну опубликованную статью.&z=<random>) к каждому повтору. Скрипт уже это делает.В SPIP есть действительно хорошая защита от SSRF, valider_url_distante(): белый список схем, частные и зарезервированные диапазоны, link-local, DNS rebinding, URL с учётными данными. Вызывающие функции запускают её перед загрузкой.
Затем recuperer_url() следует за максимум десятью редиректами и никогда не перепроверяет цель, которую собирается загрузить:
$url = suivre_lien($url, $res['location']); // 571, attacker controlled
// ... method adjustment, a log line ...
return recuperer_url($url, $options); // 582, fetched
Один 302 от хоста, прошедшего валидацию, ведёт прямиком к тому, что должен был блокировать guard, включая 169.254.169.254.
Запуск:
docker exec -w /var/www/html spiplab-app php /tmp/poc_ssrf.php
Скрипт показывает, как защитный механизм корректно отклоняет семь враждебных URL-адресов, а затем извлекает один из этих самых URL-адресов через перенаправление и выводит содержимое обратно.
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
У базы данных нет порта на хосте. Веб-контейнер слушает только loopback.
Удалите её командой docker compose down -v.
Чтобы проверить путь SSRF, вам также нужны заглушка-редиректор и сервис, доступный только изнутри:
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 лишь отправляет 302 на то, что указано в ?to=. В реальной атаке эту роль
играет публичный хост, который guard с радостью пропускает.
requests/ в каждом каталоге содержит необработанные HTTP-обмены, захваченные из сети
во время прогонов выше, по одному .req и одному .res на шаг. Вставьте .req в
Burp Repeater как есть.
Для SQLi интересующая пара — sqli-03-oracle-true (2.051 с) против
sqli-04-oracle-false (0.049 с): тот же вопрос, порог сдвинут на единицу, и
ответ — это разница во времени отклика. Тела идентичны.
Для SSRF ssrf-02-internal-hits.log — это лог доступа внутреннего
сервиса, показывающий, как SPIP приходит на хост, который он только что отказался резолвить.
SQL-инъекция: замените нестрогий regex на белый список:
} 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
)) {
Примените то же изменение к ecrire/req/sqlite_generique.php.
SSRF: проверяйте цель перенаправления перед тем, как следовать ей:
$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);
Или просто обновитесь до 4.4.18, которая содержит оба исправления.
CVE-2026-72708 был независимо со-обнаружен Franck Chevalier (Wayko) из Lexfo / Ambionics, который нашёл тот же sink, ту же точку входа без аутентификации и построил полные цепочки до удалённого выполнения кода через CVE-2026-72709 и CVE-2026-72710. Их разбор является более глубоким техническим документом, а их инструментарий доступен по адресу ambionics/spip-exploits.
CVE-2026-109378 был заявлен единолично.
MIT. Опубликовано для защитников, аудиторов и всех, кто хочет понять, почему регулярное выражение из пятнадцати символов в итоге оказалось стоящим 8.7 балла.
-- ka3n1x