Introduction
Unauthorized access vulnerability based on information disclosure in #Joomla CMS versions 4.0.0–4.2.7 has been found and registered as #CVE-2023–23752.
- Project: Joomla!
- SubProject: CMS
- Impact: Critical
- Severity: High
- Probability: High
- Versions: 4.0.0–4.2.7
- Exploit type: Incorrect Access Control
- Reported Date: 2023–02–13
- Fixed Date: 2023–02–16
- CVE Number: CVE-2023–23752
What is Joomla CMS?
Joomla is a popular open-source content management system (CMS) that allows users to build websites and online applications. It was first released in 2005 and has since grown to become one of the most widely used CMS platforms in the world, with a large and active community of users and developers.
Joomla is built on PHP and uses a MySQL database to store and manage content. It provides a user-friendly interface for managing content, templates, and extensions, making it easy for users with little technical knowledge to create and manage websites.
Joomla offers a wide range of features and functionalities, including the ability to create multiple user accounts with different levels of access, create and manage custom content types, and support for multilingual websites. It also has a large library of extensions and plugins available, allowing users to add new features and functionality to their websites.
Joomla is free to use and distribute, and it is licensed under the GNU General Public License. Its open-source nature has contributed to its popularity and has allowed it to evolve over time, as the community continues to contribute to its development and improvement.
Build the lab
Install the system and prerequisites
- Setup Ubuntu (I’m using Ubuntu server 20.04)
- Update the server
sudo apt update
- Install Apache
apt install apache2
- Start the apache service
systemctl start apache2
- Check the status of the apache service
systemctl status apache2
- Install PHP modules
apt install php php-xml php-mysql php-mbstring php-zip php-soap php-sqlite3 php-curl php-gd php-ldap php-imap php-common
- Install mysql
apt install mysql-server
- Configure the database
mysql -u root -p
create database joomla;
use joomla;
create user 'user'@localhost identified by '123456';
grant all privileges on joomla.* to 'user'@localhost;
flush privileges;
exit
- Create a directory for Joomla
cd /var/www/
mkdir joomla
cd joomla
- Download Joomla
wget https://downloads.joomla.org/cms/joomla4/4-2-6/Joomla_4-2-6-Stable-Full_Package.zip?format=zip
- Unzip the folder
unzip 'Joomla_4-2-6-Stable-Full_Package.zip?format=zip'
- Configure the permissions
chown -R www-data. ./
chmod -R 755 ./
- Create virtualhost
vim /etc/apache2/sites-available/joomla.conf
<virtualhost *:80>servername www.mhzcyber.com
documentroot /var/www/joomla/</virtualhost>
- Disable default access
a2dissite 000-default.conf
- Enable site access
a2ensite joomla.conf
- Enable rewrite module
a2enmod rewrite
- Restart Apache service
systemctl restart apache2
- Now browse the IP address of the server or the domain name
- Click “Open Administrator” and login
Background Story
What I’m trying to achieve
here is an understanding of the software flow, understand how it works, how the vulnerable endpoint gets processed, and why when we set the public parameter to true it gives us all this data finally from where we are getting this data.
What I did?
Basically, I started with reproducing the vulnerability, and from there I went to static analysis but when I got to the route() function, I needed more understanding of the flow, so I started debugging the software, following step by step.
I explained Understand the authentication bypass, Understand where the config data came from and this part made me go back and debug from the beginning starting with index.php so we understand how the data gets loaded, finally I explained understand how this data gets sent i.e. the response.
Reproduce the vulnerability
Browse the following path:
api/index.php/v1/config/application?public=true
Here we can see the leaked information, and all the config data of the database.
This will allow us to access the database if we can remotely connect to it, and if a malicious actor got the ability to access the internal network it will be able to access the database and from there you can implement multiple attacks such as accessing other accounts inside the company, spear phishing, privilege escalation ..etc.
Before we get into the static analysis, I added the methods that I went through during the debugging and the analysis trying to build a flow to make understanding this easier.
Static Analysis
Check the directory of Joomla and you can find configuration.php
I started to search for the following keywords:
- configuration.php
- JConfig
- the keywords existed in the configuration file
and I find that there is an installation folder where we can see “ConfigurationMode.php” basically the purpose of this code is to create and set the configuration file.
I was thinking but how this is getting processed? I mean where I can see what’s happening when we set the public parameter to true
First thing let’s check api/index.php
Now let’s follow '/includes/app.php'
After reading the code here, you can read only the comments and it will be enough to make sense (it’s not really relevant). However, from there the most interesting part here is the execute() function.
I followed this and I found the execute() function in CMSApplication.php
I need to study this function and whatever called functions in it, basically, this function contains the high-level logic for executing the application.
I started to check the first 4 functions.
- sanityCheckSystemVariables()
this method checks for any invalid system variables that may cause issues during the application’s execution and unsets them. If there are any invalid system variables, it aborts the application.
- setupLogging()
This method sets up the logging configuration for the Joomla CMS application. It checks the application configuration for various logging-related settings and configures loggers accordingly.
- createExtensionNamespaceMap()
This method allows the application to load a custom or default identity by creating an extension namespace map.
- doExecute()
When I tried to follow this function, first I got to here:
After that I found the main function here:
It starts with initialiseApp() which basically loads the language, sets some events, and listeners. i.e. Initialize the application.
So, from the called and used functions here the one that got my attention is route()
You can find the route function in the following path :
\libraries\src\Application\ApiApplication.php -> route
protected function route()
{
$router = $this->getContainer()->get(ApiRouter::class);
// Trigger the onBeforeApiRoute event.
PluginHelper::importPlugin('webservices');
$this->triggerEvent('onBeforeApiRoute', array(&$router, $this));
$caught404 = false;
$method = $this->input->getMethod(); try {
$this->handlePreflight($method, $router); $route = $router->parseApiRoute($method);
} catch (RouteNotFoundException $e) {
$caught404 = true;
} /**
* Now we have an API perform content negotiation to ensure we have a valid header. Assume if the route doesn't
* tell us otherwise it uses the plain JSON API
*/
$priorities = array('application/vnd.api+json'); if (!$caught404 && \array_key_exists('format', $route['vars'])) {
$priorities = $route['vars']['format'];
} $negotiator = new Negotiator(); try {
$mediaType = $negotiator->getBest($this->input->server->getString('HTTP_ACCEPT'), $priorities);
} catch (InvalidArgument $e) {
$mediaType = null;
} // If we can't find a match bail with a 406 - Not Acceptable
if ($mediaType === null) {
throw new Exception\NotAcceptable('Could not match accept header', 406);
} /** @var $mediaType Accept */
$format = $mediaType->getValue(); if (\array_key_exists($mediaType->getValue(), $this->formatMapper)) {
$format = $this->formatMapper[$mediaType->getValue()];
} $this->input->set('format', $format); if ($caught404) {
throw $e;
} $this->input->set('option', $route['vars']['component']);
$this->input->set('controller', $route['controller']);
$this->input->set('task', $route['task']); foreach ($route['vars'] as $key => $value) {
if ($key !== 'component') {
if ($this->input->getMethod() === 'POST') {
$this->input->post->set($key, $value);
} else {
$this->input->set($key, $value);
}
}
} $this->triggerEvent('onAfterApiRoute', array($this)); if (!isset($route['vars']['public']) || $route['vars']['public'] === false) {
if (!$this->login(array('username' => ''), array('silent' => true, 'action' => 'core.login.api'))) {
throw new AuthenticationFailed();
}
}
}
why this function is interesting? because it routes the application and routing is the process of examining the request environment to determine which component should receive the request. The component optional parameters are then set in the request object to be processed when the application is being dispatched.
Debugging
From here I started debugging since it started to be hard to understand the flow from the static analysis only.
Set the debugger
I’m using Phpstorm with Xdebug and I’m on ubuntu desktop.
Just download Phpstorm and start it.
After that in Chrome, install this extension
Xdebug helper
Link: https://chrome.google.com/webstore/detail/xdebug-helper/eadndfjplgieldjbigjakmdgkmoaaaoc
After you install it, go to the link, click on the extension, and click debug
Now you will get a message in phpstorem that there is a request coming.
NOTE: you maybe need to restart chrome browser.
You can follow this video for more information:
As debugging and reverse engineering a binary program, usually you would set a breakpoint on the main function. we will do the same here, and in our case index.php can be considered as the main, and it starts running when it runs the app.php which all executable code should be triggered through it.
Understand how the data gets loaded
While you are stepping into the program, you will notice line 25 in app.php where it’s including framework.php
We can see here that there is a pre-loaded configuration and it’s going to load it.
Now in configuration.php we can see all of it.
Here we can see that the data in configuration.php got assigned to the variable $config.
Here I listed the important methods that I noticed the program going through
NOTE: those are not all the methods/functions but those are the most obvious and clarify how the flow works.
route()
- getContainer()
- This will get DI container, and prepare it.
- In Joomla CMS, a Dependency Injection (DI) container is a software component that manages the instantiation and dependency resolution of objects in the application. It is a design pattern that allows developers to write modular, decoupled, and reusable code.
- The Joomla DI container is based on the PHP-DI library, which provides a simple and flexible way to manage object dependencies in a Joomla application. The DI container is used to instantiate and manage objects and to inject dependencies into them.
- getMethod()
- This method will get the HTTP request method.
- When you follow it, you will notice it’s going to __get function, and we can see that the $method variable is set to GET.
- handlePreflight()
this handles the preflight requests. A preflight request is a small request that is sent by the browser before the actual request. It contains information like which HTTP method is used, as well as if any custom HTTP headers are present. The preflight gives the server a chance to examine what the actual request will look like before it’s made.
Basically, it will check if this is an OPTIONS request or if CORS is enabled, if not it does nothing.
- parseApiRoute()
This method parses the given route and returns the name of a controller mapped to the given route.
it requires a method parameter, Request method to match. One of GET, POST, PUT, DELETE, HEAD, OPTIONS, TRACE, or PATCH.
it returns an array containing the controller and the matched variables. and if some error happened it will call InvalidArgumentException which is an exception that is thrown when an inappropriate argument is passed to a function. This could be because of an unexpected data type or invalid data.
— getRoutePath()
This method will get the path from the route and remove any leading or trailing slash.
This method uses getInstance() which returns the global Uri object, only creating it if it doesn’t already exist, and also getPath() which gets the URL path string. here are the values of both:
Now back to parseApiRoute(), we have this line
$query = Uri::getInstance()->getQuery(true);
and this will retrieve the parameter public and its value true
After that, it goes through a for loop to iterate through all of the known routes looking for a match, and here we can see the matches.
From there going back to route()
and you can see that all the variables are set.
Now it will trigger an event which means it will get the event name ‘onAfterApiRoute’ and it will set some values.
$this->triggerEvent('onAfterApiRoute', array($this));
Understand the authentication bypass
After that we go to the if statement that checks if the $route
variable
contains a key 'public'
and if its value is false
. If the key is not set or its value is false
, the code attempts to log in the user by calling the $this->login()
method with two parameters: an empty array for the username and an array containing two additional parameters: 'silent' => true
and 'action' => 'core.login.api'
.
If the login fails, the code throws an AuthenticationFailed
exception.
But if the 'public'
key is set to a value of true
in the $route
variable, the first part of the if
condition in the code snippet will evaluate to false
. This means that the code inside the if
block will not be executed, and the user will not be required to log in.
Therefore, if 'public'
is set to true
, the user will have access to the route without the need for authentication.
and this is why we can bypass the authentication or no authentication required to access the data.
Understand where the config data came from
Now going back to the doExecute() function, we reached to dispatch() method.
when I reached here I was still trying to understand how the data gets retrieved, and while I’m stepping into dispatch() method, I got here:
As you can notice the $component variable set to “config” and here I started to follow config and I found the following:
libraries/vendor/joomla/application/src/AbstractApplication.php
and this is what pushes me to go from the beginning and start debugging from index.php to Unserstand how the data gets loaded.
a note, $config variable, and the data are already assigned as we saw in the Understand how the data gets loaded section.
understand how this data gets sent
now we need to understand how this data gets sent.
going back to dispatch() basically, it’s responsible for rendering a particular component (specified by $component
or via the 'option' HTTP GET parameter) and setting up the associated document buffer, while also triggering a plugin event after the component has been dispatched.
now back to execute() it will render the output and rendering is the process of pushing the document buffers into the template placeholders, retrieving data from the document, and pushing it into the application response buffer, and here basically you will see the program sets the body content and prepare it.
after that, we will see the respond() method called and this method prepares the headers and the response to be sent.
after it will trigger the onAfterRespond event which means it’s the end but one last touch is to shut down the registered function for handling PHP fatal errors. using handleFatalError() function and you will notice that it will go to DatabaseDriver.php to __destructor() to disconnect from the database.
Mitigation
Upgrade to version 4.2.8
Final Thoughts
This was a really hard one to debug and analyze, and that’s because the way Joomla CMS is developed they break it into small components, methods ..etc, and basically they go through a lot of loops to break each request, take the input through some regex check, and also initiate all the needed components/variables for this request.
The explanation here was not straightforward, not like going step by step, there’s some go back and forth with the analysis and this is intended since I wanted to give you a window to see closely to some level what I went through during the analysis.
I believe it would be hard to really understand the whole flow not only the vulnerability itself but also the program if you don’t debug it by yourself and step into it step by step. However, I tried to give more of a general look and go more in detail with the root cause of the vulnerability itself.
Resources:
- https://nsfocusglobal.com/joomla-unauthorized-access-vulnerability-cve-2023-23752-notice/
- https://developer.joomla.org/security-centre/894-20230201-core-improper-access-check-in-webservice-endpoints.html
- https://github.com/joomla-framework/di
- https://github.com/joomla-framework/di/blob/1.x-dev/docs/why-dependency-injection.md