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. In addition, 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. However, 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. We decided to target 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 the password reset 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.

Password reset link generation

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! If we look in URI.php we see that the password reset link uses 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 under attacker’s control. The malicious server setup by the attacker will collect the password reset token and set a new password. Time to do a little dance! Obviously, you can obfuscate the hostname, 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 one either.

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. For mime types they use a a similar approach for validation 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 is CVE-2021-26032 and we recommend you to download Joomla 3.9.27 or later.

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 you shouldn’t not use it 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. They consider it more of an UX improvement (just don’t call it a vulnerability). 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 they added documentation on their site for this issue 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 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. Joomla will fix the password reset vulnerability in the next weeks with a “trusted_hosts” configuration

07.06.2021 – Fortbridge releases the write-up

References
Drupal password reset poisoning vulnerability.
Joomla password reset poisoning vulnerability leads to full compromise.
Concrete CMS Part2 – Password reset poisoning vulnerability.

About Post Author