From a GLPI patch bypass to RCE
Introduction
GLPI is a popular software used by companies, mainly in France. GLPI is usually used for two main purposes. Firstly it allows companies to see the inventory of their different equipment (such as: computers, software, printers, etc…). Secondly it is used for its ticketing system, allowing users to create tickets about their issues. It also has different roles for each user, those who can only create tickets (low privileges user), and those who get central access (again there are different roles here). During an internal penetration test, chances are that if you got an account, you will be a low privilege user, known as Self-Service.
Introduction
GLPI is a popular software used by companies, mainly in France. GLPI is usually used for two main purposes. Firstly it allows companies to see the inventory of their different equipment (such as: computers, software, printers, etc…). Secondly it is used for its ticketing system, allowing users to create tickets about their issues. It also has different roles for each user, those who can only create tickets (low privileges user), and those who get central access (again there are different roles here). During an internal penetration test, chances are that if you got an account, you will be a low privilege user, known as Self-Service.
Moreover, this software is often configured to bind to Active Directory through LDAP, and thus it is a great target for an attacker. You might get a sensitive account, if you can take control of the server. Also, it will give you a lot of information about the domain thanks to its inventory feature (if in use).
In this post I will describe how I found a patch bypass to re-exploit a SQL injection vulnerability (previously found by brent-hopkins), along with how to take it further to achieve RCE on a vulnerable GLPI instance.
Technologies
GLPI is open-source, and uses the following stack:
- PHP >= 7.4 (usually php > 8.0)
- MySQL / MariaDB
Most of the code is custom, ie no frameworks are used. For rendering templates Twig is used, however it is not made using the MVC architecture. More recently a Firewall class was added in order to perform global access checks (@since 10.0.10).
Perform global access check
For database communication they have chosen to implement their own ORM (object-relational mapping). Here the ORM is a Parent Class named CommonDBTM (Common DataBase Table Manager Class), that allows easy communication between an object and the database.
To use it, create a class that extends the CommonDBTM class, and your class will be able to use Create, Read, Update and Delete operations in the database (often referred as CRUD). This architectural pattern is known as the Active Record Pattern.
SQL Injection
Finding the SQL Injection
As said before, GLPI uses its own ORM, and some vulnerabilities have been reported in the past. In fact I reported some SQL Injection vulnerabilities in the past, and here is a link to one of the PoC’s CVE-2023-41320.
The reported SQL injection was identified here:
SQL Injection
And here is the patch the developer’s applied:
Patch for SQL Injection
The class Sanitizer::dbEscape protects against SQL injection. And the unsanitize method removes protection against XSS and SQL injection.
That is weird. In order to prevent SQL injection here, we must remove the protection against SQL injection first?
In fact the Sanitizer class acts as a (kind of) middleware on GLPI that protects against some attacks:
Protections against SQL Injection / XSS
As you can see above, globals are protected against some attacks because this file is loaded at the beginning of each request. So how can we perform SQL injection if it is already escaped? Here comes an interesting thing. To answer that, let’s first take a look at the json_encode function.
This function must be careful with some characters, namely the " and the \ characters as they could break JSON. So here is what happens in GLPI. First you send an input containing a ', then this simple quote is protected using addslashes, then the \ is escaped by the json_encode function to avoid breaking potential JSON:
SQL Injection after json_encode
And then, the double \ prevents addslashes() from correctly escaping the simple quotes.
That is basically how I found the SQL injection in here. I was done looking for vulnerabilities in GLPI, but then I saw that someone found the exact same vulnerability in another file. I saw this in the Quarkslab article.
Here is CVE-2023-43813:
SQL Injection
You might see the problem here! exportArrayToDB is nothing more than an alias for json_encode. So the pattern is exactly the same as the previous SQL injection.
And above all, here is the patch:
Patch for the CVE-2023-43813
The patch here adds a check on the $_POST variable, that could have been altered. When I saw this, I knew the patch worked (or at least it seems), but there is still SQL injection! In fact the SQL injection was not only in the $_POST variable, if we look carefully, we can see that the attack is also possible through the $_SESSION variable. However, it seems that the $_SESSION variable cannot be updated directly. Now let’s deep dive into the GLPI CRUD operations.
Bypassing the patch
The patch protects the variable sent through the web to this page. Nevertheless, the SQL injection is still here through a variable we cannot set.
How do session variables work in GLPI?
GLPI initialises the SESSION of a user with the data within the database like this (method: Session::init):
Session Initialization
$auth->user->fields is just a link to the value $field in the table glpi_users.
So almost every field of the glpi_users table will be stored in our $_SESSION, with the prefix glpi.
With user_pref_field defined in inc/define.php, it contains the savedsearches_pinned field that we need to store our payload. Before it was possible to exploit this by using the $_POST variable. However, it is not possible to poison this variable anymore, so we need to update this value in the database directly as shown in the Session::init method, and the savedsearches_pinned field will help us get there.
How does the update of your field work in GLPI?
When updating a User object through the preferences menu, here is what really happens:
Preferences menu
The variables that you want to update are sent through a POST request, and are processed like this:
User update
The user object is updated given the whole $_POST variable. You can already think of a vulnerability here. Someone told me when looking for a vulnerability: Always follow your intuition. So here we are, it looks like there is a mass assignment opportunity given a Superglobal. Let’s give it a try, and add the variable we want to update through the POST request directly:
Original request (simplified)
Request modified
And here is the result in the database:
Poisoned database
Woohoo, we successfully stored what we wanted through a Mass Assignment vulnerability, to a table and field that will be used in the session later.
In the code we can see that a lot of columns can be updated through this page. The code responsible for this is at User::prepareInputForUpdate in src/User.php.
Mass Assignment
Through this the Session’s variable named savedsearches_pinned can be updated.
For older versions you need to update the DB and login again, here is the code responsible for the DB update (method CommonDBTM::update):
Field update
Field set value
Through this you updated the database, and thus can login again to set up the $_SESSION trap.
Exploitation
Now we need to try SQL injection! Note that the vulnerable savedsearches_pinned endpoint works with arrays, so we need to store a JSON string in order to make our payload work. The original SQL request looks like this:
UPDATE `glpi_users` SET `savedsearches_pinned` = '<SQL Injection here>'
Let’s try a simple sql injection, something like this:
UPDATE `glpi_users` SET `savedsearches_pinned` = '', phone='0606060606'
SQL Payload
And here is the payload stored in the database:
SQL Payload prepared in database
[...]