Joomla password reset vulnerability and a stored XSS for full compromise

Intro

Joomla is one of the most popular CMS-es with over 1.5 million installations world-wide. We pentested Joomla 3.9.24 and found a password reset vulnerability which we chained with a set of vulnerabilities and features to achieve full compromise of the underlying server.

Joomla has a strong OOP architecture and a large codebase. Strong input validation is applied everywhere, prepared statements are used to protect against sql injections and also type casting is used where integers are required. Also, by reviewing the code we noticed that defense in depth measures are applied in many places, which is a good sign from a defensive perspective, but it also means we will have to work harder to achieve our goals.

Joomla password reset vulnerability

To set up the stage, let’s discuss a bit about user roles in Joomla. There are 2 interesting roles, one is “admin” and the other one is “super admin”. As you guessed it’s the “super admin” we really want in the end, but there are a couple of steps in order to get there. One of the things we targeted is the password reset functionality. However, you can’t reset the password of a “super admin” the way you would reset it for all other users, the Joomla developers have already thought of that, and they completely removed this functionality for “super admin”. Reducing the attack surface is always a good idea, so kudos to them.

However, we can still reset the password for any other user that is not a “super admin”. Let’s target a regular “admin” in that case. We obviously did a quick Burp scan and found nothing there….not really surprising, right? Most probably everyone else ran a Burp scan before we did and, all those low hanging fruits have been eliminated.

So, we started doing code reviews. Reviewing Joomla source code is not exactly easy, due to the complex OOP architecture and large codebase.  You have to dig really deep in order to find something. So, the truth is we were ready to give up and move on when we remembered about albinowax‘ old technique of poisoning the password reset links. The password reset process starts in the reset.php model:

$link = 'index.php?option=com_users&view=reset&layout=confirm&token=' . $token;

// Put together the email template data.
$data = $user->getProperties();
$data['fromname'] = $config->get('fromname');
$data['mailfrom'] = $config->get('mailfrom');
$data['sitename'] = $config->get('sitename');
$data['link_text'] = JRoute::_($link, false, $mode);
$data['link_html'] = JRoute::_($link, true, $mode);
$data['token'] = $token;

We need to dig deep and check how the domain is added to the reset link. Here’s how the code looks like in URI.php:

if (!empty($_SERVER['PHP_SELF']) && !empty($_SERVER['REQUEST_URI']))
{
  // To build the entire URI we need to prepend the protocol, and the http host
  // to the URI string.
   $theURI = 'http' . $https . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
}

Bingo! We found that the password reset link is created in URI.php by using the HOST header, thus it’s vulnerable to host header poisoning. Let’s try this with Repeater and see if it actually works:

Joomla password reset link hijacking

And a few moments later this glorious email arrives to my inbox

Host header poisoning

As you can see the host is controlled by the attacker, the victim will be taken to a malicious server which will collect the password reset token and set a new password. Time to do a little dance! Obviously, the hostname can be obfuscated, there’s plenty of resources on the internet on how to do that.

But why did the automated scan not find this bug? Let’s check the code and see where the issue is. Bellow, is the reset.php file which does validation for this scenario. Another defense in depth measure is that you’re only allowed to reset your password for $maxCount times per hour.  $maxCount variable is set to 10 in the config.xml file.

if ($hoursSinceLastReset > $resetHours)
{
// If it's been long enough, start a new reset count
$user->lastResetTime = JFactory::getDate()->toSql();
$user->resetCount = 1;
}
elseif ($user->resetCount < $maxCount)
{
// If we are under the max count, just increment the counter
++$user->resetCount;
}
else
{
// At this point, we know we have exceeded the maximum resets for the time period
$result = false;
}

This makes sense now.  You can’t find this vulnerability automatically if your scanner sends 100’s/1000’s of requests and your HOST Header poisoning attack is not among the first 10 requests (most likely it will not be) . We have reached out to Burp Suite to discuss this issue. Quite amazing that this bug went undiscovered for years, but it wouldn’t be the first deep hidden bug that goes undetected for ages.

Once an unsuspecting user clicks on the link, ( or some AV/EDR software scans the user’s inbox) you will get an admin’s reset token and reset his password. Don’t get super excited yet, as we said there are 2 types of “admin” roles in Joomla, the regular admin and the Super Admin, we will discuss privilege escalation in the next section.  A python POC for this vulnerability is located here.

Privilege Escalation via stored XSS

So, what’s next? Let’s try to mess with the upload media functionality. We tried many dirty tricks to upload a php file, close but no cigar 🙁 . Reviewing the code it was clear that they’re doing some solid validations on file name, extensions, file content etc.

Anyway, in the end while looking for functionality that we can abuse, we discovered that the admin user has actually the permission to disable some of the upload restrictions.  Just some of the restrictions, because the Joomla developers have done a great job at hard coding many extensions(.php, php5, php7, phtml etc), which you can’t upload. On top of that, they also use a whitelist of hardcoded extensions which are allowed. Similar approach is done for mime types and the filename has solid alpha numeric validation.

One thing they haven’t hardcoded in their blacklist though, is the .html extensions. As you see as a regular admin, we were able to add “.html” as a “legal extension” and also disable “Restrict Uploads”.

whitelist the ".html" extension.

The “php” extensions that we tried to whitelist are completely ignored, however we have managed to whitelist the “.html” extension. Now we can go ahead and upload a “html” file containing an XSS payload which will target the “super admin” user.

Joomla Privelege escalation via stored XSS

You can use the internal messaging feature to deliver the XSS payload to the Super Admin or you can embed the link in the website articles, comments etc. Our XSS exploit will elevate our privileges to “Super Admin” and once the victim visits the link it’s game over.  You can check out our PoC here. You will need to configure some variables at the top with the username and user_id of the admin user you already compromised. This bug was assigned CVE-2021-26032 and was fixed in Joomla 3.9.27.

Full compromise

Full compromise is a no-brainer really, most CMS-es support the capability of uploading custom themes/plugins etc. We wrote a very simple custom plugin which gave us RCE. This is for PoC purposes only and should not be used as such in a real environment. You can find our PoC here.

Mitigations

After around 3 months of waiting, the password reset vulnerability is still not fixed, being considered more of an UX improvement. The Joomla team plans to release a PR in the next few weeks, which will fix the issue by default, but only for new sites, because they said that they don’t want to risk breaking the existing ones and that this issue affects only non-vhost setups. However, the good news is that at least the patch will include a post installation notice to warn the existing users about the issue so that they can fix the old websites as well. The Joomla team recommends setting the $live_site variable in configuration.php file to your website’s domain, which fixes the password reset vulnerability.

Timeline

11.02.2021 Fortbridge discloses password reset vulnerability to Joomla

03.03.2021 Joomla replies that this issue is now documented on their site, and they recommend using $live_site variable

08.03 2021 Fortbridge follows up with another XSS vulnerability which can help escalate privileges from Admin to Super Admin

26.03.2021 Joomla agrees to fix the issues on the next release, 13.04.2021

13.04.2021 Deadline is missed, vulnerabilities still not fixed, Fortbridge allows another extension until the next release cycle

25.05.2021 – Joomla releases patch for the XSS vulnerability in version 3.9.27, the password reset vulnerability will be fixed in the next weeks with a “trusted_hosts” configuration

07.06.2021 – Fortbridge releases the write-up