CVE-2020-5248 (PHP, published 2020-05-12T16:15Z)
Description: value: GLPI before before version 9.4.6 has a vulnerability involving a default encryption key. GLPIKEY is public and is used on every instance. This means anyone can decrypt sensitive data stored using this key. It is possible to change the key before installing GLPI. But on existing instances, data must be reencrypted with the new key. Problem is we can not know which columns or rows in the database are using that; espcially from plugins. Changing the key without updating data would lend in bad password sent from glpi; but storing them again from the UI will work.
Severity: MEDIUM (CVSS v3 base score: 5.3)
Weakness: CWE-798 \u2014 Use of Hard-coded Credentials
The product contains hard-coded credentials, such as a password or cryptographic key.
Repository: https://github.com/glpi-project/glpi
Files changed:
.gitignore
ajax/mailcollector.php
inc/auth.class.php
inc/authldap.class.php
inc/config.class.php
inc/console/security/changekeycommand.class.php
inc/glpikey.class.php
inc/glpimailer.class.php
inc/mailcollector.class.php
inc/rssfeed.class.php
inc/toolbox.class.php
inc/update.class.php
install/install.php
status.php
tests/functionnal/AuthLdap.php
Commit message: Deprecate GLPIKEY usage

CVE-2020-5248

Deprecate GLPIKEY usage, and replace it with key file per instance.
Add a command to generate new key, and update database.
Add plugins hooks to register fields or configuration entries to be
handled when updating db.


Vulnerable code:
               $input["passwd"] = Toolbox::encrypt(stripslashes($input["passwd"]), GLPIKEY);
                                                  Toolbox::decrypt($ldap_method["rootdn_passwd"],
                                                                   GLPIKEY),
            $input["rootdn_passwd"] = Toolbox::encrypt(stripslashes($input["rootdn_passwd"]),
                                                       GLPIKEY);
                                  Toolbox::decrypt($config_ldap->fields['rootdn_passwd'], GLPIKEY),
                                    Toolbox::decrypt($this->fields['rootdn_passwd'], GLPIKEY),
                                  Toolbox::decrypt($ldap_method['rootdn_passwd'], GLPIKEY),
                                        Toolbox::decrypt($ldap_method['rootdn_passwd'], GLPIKEY),
                                Toolbox::decrypt($authldap->getField('rootdn_passwd'), GLPIKEY),
         $input["rootdn_passwd"] = Toolbox::encrypt(stripslashes($input["rootdn_passwd"]), GLPIKEY);
            $input["smtp_passwd"] = Toolbox::encrypt(stripslashes($input["smtp_passwd"]), GLPIKEY);
            $input["proxy_passwd"] = Toolbox::encrypt(stripslashes($input["proxy_passwd"]),
                                                      GLPIKEY);
            $this->Password = Toolbox::decrypt($CFG_GLPI['smtp_passwd'], GLPIKEY);
            $input["passwd"] = Toolbox::encrypt(stripslashes($input["passwd"]), GLPIKEY);
            $input["passwd"] = Toolbox::encrypt(stripslashes($input["passwd"]), GLPIKEY);
                                     Toolbox::decrypt($this->fields['passwd'], GLPIKEY),
                                        Toolbox::decrypt($this->fields['passwd'], GLPIKEY),
                                             Toolbox::decrypt($CFG_GLPI["proxy_passwd"],
                                                              GLPIKEY);
   static function encrypt($string, $key) {
   static function decrypt($string, $key) {
               CURLOPT_PROXYUSERPWD => $CFG_GLPI["proxy_user"] . ":" . self::decrypt($CFG_GLPI["proxy_passwd"], GLPIKEY),
         die('Upgrade from command line is not supported before 0.72.3!');
                                            Toolbox::decrypt($method["rootdn_passwd"],
                                            GLPIKEY))) {
      //Expected value to be encrypted using GLPIKEY key
      $expected = \Toolbox::encrypt(stripslashes($password), GLPIKEY);
      //Expected value to be encrypted using GLPIKEY key
      $expected = \Toolbox::encrypt(stripslashes($password), GLPIKEY);

Fixed code:
/config/glpi.key
               $input["passwd"] = Toolbox::encrypt(stripslashes($input["passwd"]));
                                                  Toolbox::decrypt($ldap_method["rootdn_passwd"]),
            $input["rootdn_passwd"] = Toolbox::encrypt(stripslashes($input["rootdn_passwd"]));
                                  Toolbox::decrypt($config_ldap->fields['rootdn_passwd']),
                                    Toolbox::decrypt($this->fields['rootdn_passwd']),
                                  Toolbox::decrypt($ldap_method['rootdn_passwd']),
                                        Toolbox::decrypt($ldap_method['rootdn_passwd']),
                                Toolbox::decrypt($authldap->getField('rootdn_passwd')),
         $input["rootdn_passwd"] = Toolbox::encrypt(stripslashes($input["rootdn_passwd"]));
            $input["smtp_passwd"] = Toolbox::encrypt(stripslashes($input["smtp_passwd"]));
            $input["proxy_passwd"] = Toolbox::encrypt(stripslashes($input["proxy_passwd"]));
<?php
/**
 * ---------------------------------------------------------------------
 * GLPI - Gestionnaire Libre de Parc Informatique
 * Copyright (C) 2015-2018 Teclib' and contributors.
 *
 * http://glpi-project.org
 *
 * based on GLPI - Gestionnaire Libre de Parc Informatique
 * Copyright (C) 2003-2014 by the INDEPNET Development Team.
 *
 * ---------------------------------------------------------------------
 *
 * LICENSE
 *
 * This file is part of GLPI.
 *
 * GLPI is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * GLPI is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with GLPI. If not, see <http://www.gnu.org/licenses/>.
 * ---------------------------------------------------------------------
 */

namespace Glpi\Console\Security;

if (!defined('GLPI_ROOT')) {
   die("Sorry. You can't access this file directly");
}

use Glpi\Console\AbstractCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use GLPIKey;

class ChangekeyCommand extends AbstractCommand {
   /**
    * Error code returned when unable to renew key.
    *
    * @var integer
    */
   const ERROR_UNABLE_TO_RENEW_KEY = 1;

   protected function configure() {
      parent::configure();

      $this->setName('glpi:security:change_key');
      $this->setDescription(__('Change password storage key and update values in database.'));
   }

   protected function execute(InputInterface $input, OutputInterface $output) {
      $glpikey = new GLPIKey();

      $fields = $glpikey->getFields();
      $configs = $glpikey->getConfigs();
      $conf_count = 0;
      foreach ($configs as $config) {
         $conf_count += count($config);
      }

      $output->writeln(
         sprintf(
            '<info>' . __('Found %1$s field(s) and %2$s configuration entries requiring migration.') . '</info>',
            count($fields),
            $conf_count
         )
      );

      if (!$input->getOption('no-interaction')) {
         // Ask for confirmation (unless --no-interaction)
         $question_helper = $this->getHelper('question');
         $run = $question_helper->ask(
            $input,
            $output,
            new ConfirmationQuestion(__('Do you want to continue ?') . ' [Yes/no]', true)
         );
         if (!$run) {
            $output->writeln(
               '<comment>' . __('Aborted.') . '</comment>',
               OutputInterface::VERBOSITY_VERBOSE
            );
            return 0;
         }
      }

      $created = $glpikey->generate();
      if (!$created) {
         $output->writeln(
            '<error>' . __('Unable to change security key!') . '</error>',
            OutputInterface::VERBOSITY_QUIET
         );
         return self::ERROR_UNABLE_TO_RENEW_KEY;
      }

      $this->output->write(PHP_EOL);

      $output->writeln('<info>' . __('New security key generated; database updated.') . '</info>');

      return 0; // Success
   }
}
<?php
/**
 * ---------------------------------------------------------------------
 * GLPI - Gestionnaire Libre de Parc Informatique
 * Copyright (C) 2015-2018 Teclib' and contributors.
 *
 * http://glpi-project.org
 *
 * based on GLPI - Gestionnaire Libre de Parc Informatique
 * Copyright (C) 2003-2014 by the INDEPNET Development Team.
 *
 * ---------------------------------------------------------------------
 *
 * LICENSE
 *
 * This file is part of GLPI.
 *
 * GLPI is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * GLPI is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with GLPI. If not, see <http://www.gnu.org/licenses/>.
 * ---------------------------------------------------------------------
 */

if (!defined('GLPI_ROOT')) {
   die("Sorry. You can't access this file directly");
}

/**
 *  GLPI security key
**/
class GLPIKey {
   /**
    * Key file path.
    *
    * @var string
    */
   private $keyfile;

   /**
    * List of crypted DB fields.
    *
    * @var array
    */
   protected $fields = [
      'glpi_mailcollectors.passwd',
      'glpi_authldaps.rootdn_passwd'
   ];

   /**
    * List of crypted configuration values.
    * Each key corresponds to a configuration context, and contains list of configs names.
    *
    * @var array
    */
   protected $configs = [
      'core'   => [
         'smtp_passwd',
         'proxy_passwd'
      ]
   ];

   public function __construct() {
      $this->keyfile = GLPI_CONFIG_DIR . '/glpi.key';
   }

   /**
    * Check if GLPI security key used for decryptable passwords exists
    *
    * @return string
    */
   public function keyExists() {
      return file_exists($this->keyfile) && !empty($this->get());
   }

   /**
    * Get GLPI security key used for decryptable passwords
    *
    * @return string
    */
   public function get() {
      $key = GLPIKEY;
      if (file_exists($this->keyfile)) {
         //load key from existing config file
         $key = file_get_contents($this->keyfile);
      }
      return $key;
   }

   /**
    * Generate GLPI security key used for decryptable passwords
    * and update values in DB if necessary.
    *
    * @return boolean
    */
   public function generate() {
      global $DB;

      $current_key = file_exists($this->keyfile) ? $this->get() : GLPIKEY;

      $success = (bool)file_put_contents($this->keyfile, Toolbox::getRandomString(50));
      if (!$success) {
         return false;
      }

      if ($DB instanceof DBmysql) {
         return $this->migrateFieldsInDb($current_key)
            && $this->migrateConfigsInDb($current_key);
      }

      return true;
   }

   /**
    * Get fields
    *
    * @return array
    */
   public function getFields() {
      global $PLUGIN_HOOKS;

      $fields = $this->fields;
      if (isset($PLUGIN_HOOKS['secured_fields'])) {
         foreach ($PLUGIN_HOOKS['secured_fields'] as $plugfields) {
            $fields = array_merge($fields, $plugfields);
         }
      }

      return $fields;
   }

   /**
    * Get configs
    *
    * @return array
    */
   public function getConfigs() {
      global $PLUGIN_HOOKS;

      $configs = $this->configs;

      if (isset($PLUGIN_HOOKS['secured_configs'])) {
         foreach ($PLUGIN_HOOKS['secured_configs'] as $plugin => $plugconfigs) {
            $configs['plugin:' . $plugin] = $plugconfigs;
         }
      }

      return $configs;
   }

   /**
    * Migrate fields in database
    *
    * @param string $current_key Current key
    *
    * @return void
    */
   protected function migrateFieldsInDb($current_key) {
      global $DB;

      $success = true;

      foreach ($this->getFields() as $field) {
         list($table, $column) = explode('.', $field);

         $iterator = $DB->request([
            'SELECT' => ['id', $column],
            'FROM'   => $table
         ]);

         while ($success && $row = $iterator->next()) {
            $pass = Toolbox::encrypt(Toolbox::decrypt($row[$column], $current_key));
            $success = $DB->update(
               $table,
               [$field  => $pass],
               ['id'    => $row['id']]
            );
         }
      }

      return $success;
   }

   /**
    * Migrate configurations in database
    *
    * @param string $current_key Current key
    *
    * @return boolean
    */
   protected function migrateConfigsInDb($current_key) {
      global $DB;

      $success = true;

      foreach ($this->getConfigs() as $context => $names) {
         $iterator = $DB->request([
            'FROM'   => Config::getTable(),
            'WHERE'  => [
               'context'   => $context,
               'name'      => $names
            ]
         ]);

         while ($success && $row = $iterator->next()) {
            $pass = Toolbox::encrypt(Toolbox::decrypt($row['value'], $current_key));
            $success = $DB->update(
               Config::getTable(),
               ['value' => $pass],
               ['id'    => $row['id']]
            );
         }
      }

      return $success;
   }
}
            $this->Password = Toolbox::decrypt($CFG_GLPI['smtp_passwd']);
            $input["passwd"] = Toolbox::encrypt(stripslashes($input["passwd"]));
            $input["passwd"] = Toolbox::encrypt(stripslashes($input["passwd"]));
                                     Toolbox::decrypt($this->fields['passwd']),
                                        Toolbox::decrypt($this->fields['passwd']),
                                             Toolbox::decrypt($CFG_GLPI["proxy_passwd"]);
   static function encrypt($string, $key = null) {

      if ($key === null) {
         $key = self::getGlpiSecKey();
      }

      if ($key === GLPIKEY && !defined('TU_USER')) {
         self::deprecated('Using GLPIKEY is not secure!');
      }
   static function decrypt($string, $key = null) {

      if ($key === null) {
         $key = self::getGlpiSecKey();
      }
   /**
    * Get GLPI security key used for decryptable passwords
    *
    * Will read key from config/glpi.key if present.
    * For 9.4 branch, this will defaults to GLPIKEY.
    *
    * @return string
    */
   public static function getGlpiSecKey() {
      $glpikey = new GLPIKey();
      return $glpikey->get();
   }

               CURLOPT_PROXYUSERPWD => $CFG_GLPI["proxy_user"] . ":" . self::decrypt($CFG_GLPI["proxy_passwd"]),
         echo 'Upgrade from command line is not supported before 0.72.3!';
         die(1);

      //generate security key if missing, and update db
      $glpikey = new GLPIKey();
      if (!$glpikey->keyExists() && !$glpikey->generate()) {
         $this->migration->displayWarning(__('Unable to create security key file!'), true);
      }
   //create security key
   $glpikey = new GLPIKey();
   $secured = $glpikey->keyExists();
   if (!$secured) {
      $secured = $glpikey->generate();
   }
   if (!$secured) {
      echo "<p><strong>".__('Security key cannot be generated!')."</strong></p>";
      prev_form($host, $user, $password);
      return;
   }

                                            Toolbox::decrypt($method["rootdn_passwd"]))) {
      //Expected value to be encrypted using current  key
      $expected = \Toolbox::encrypt(stripslashes($password));
      //Expected value to be encrypted using current key
      $expected = \Toolbox::encrypt(stripslashes($password));