Serializzazione e deserializzazione

La serializzazione è un processo che permette di convertire un oggetto complesso in una rappresentazione da cui si possa ricalcolare l'oggetto originario. Un oggetto può essere serializzato per essere memorizzato, ad esempio su file, o trasmesso in rete.

La deserializzazione è il processo inverso che, a partire dalla rappresentazione serializzata di un oggetto, permette di ricostruire quest'ultimo.

Al giorno d'oggi il formato più utilizzato per la serializzazione di oggetti e dati è JSON. JSON permette di serializzare oggetti in un formato leggibile e facilmente modificabile anche dall'essere umano, senza rappresentazioni binari. Una classe così definita in pseudocodice

class Author {
	String first_name;
	String last_name;
	Date date_of_birth;
	String[] books;
}

potrebbe avere un'istanza così serializzata in JSON:

{
	"first_name": "Isaac",
	"last_name": "Asimov",
	"date_of_birth": "1992-04-06T00:00:00.000Z",
	"books": [
		"The Caves of Steel",
		"The Naked Sun",
		...
	]
}

JSON tuttavia possiede diverse limitazioni, ad esempio si può notare come in nessun modo sia indicato che l'oggetto serializzato nell'esempio precedente sia di tipo class Author, e queste derivano dal fatto che il formato è completamente indipendente dal linguaggio di programmazione utilizzato, e pertanto non può in nessun modo essere legato alle sue funzionalità (come la definizione di classe).

Per questa ragione, molti linguaggi di programmazione presentano formati di serializzazione nativi che, a discapito di una maggiore interoperabilità, supportano maggiori funzionalità rispetto a un formato più semplice quale JSON, come ad esempio il supporto dei tipi nativi delle variabili e la possibilità di modificare in base alle esigenze i meccanismi di serializzazione e deserializzazione.

Fingiamo di voler espandere la classe dell'esempio precedente nel seguente modo:

class Author {
	String first_name;
	String last_name;
	String full_name;
	Date date_of_birth;
	String[] books;
	int number_of_books;
}

le due nuove variabili aggiunte non hanno altro che un valore derivato da altre variabili (full_name sarà la concatenazione di first_name e last_name, number_of_books la lunghezza della lista books). Per questo, non è davvero necessario salvarne il valore in fase di serializzazione, basta che sia possibile espandere il processo di deserializzazione per calcolarne il valore correttamente. In questo caso si tratterebbe di una semplice ottimizzazione di spazio, ma a volte è proprio necessario eseguire logiche di business più complesse ogni volta che un oggetto viene serializzato/deserializzato. Per queste ragioni, la maggior parte dei linguaggi che offrono un formato di serializzazione nativo, permettono anche di espandere i meccanismi di serializzazione e deserializzazione di specifici oggetti.

Tuttavia, le funzionalità dei formati di serializzazione nativi spesso possono portare a vulnerabilità quando operano su dati malevoli. In alcuni casi, questi attacchi possono portare anche a esecuzione di codice arbitrario (RCE: remote code execution).

Java

Java permette di rendere oggetti serializzabili tramite l'interfaccia Serializable, con la quale si possono sovrascrivere i metodi responsabili della serializzazione e deserializzazione. L'esempio precedente, con gli attributi derivati full_name e number_of_books potrebbe essere così implementato:

class Author implements Serializable {
	String first_name;
	String last_name;
	String full_name;
	Date date_of_birth;
	String[] books;
	int number_of_books;

	private void writeObject(ObjectOutputStream stream) throws IOException {
		// Salviamo solo gli attributi strettamente necessari
		stream.writeObject(first_name);
		stream.writeObject(last_name);
		stream.writeObject(date_of_birth);
		stream.writeObject(books);
	}

	private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
		first_name = (String) stream.readObject();
		last_name = (String) stream.readObject();
		date_of_birth = (Date) stream.readObject();
		books = (String[]) stream.readObject();

		// Calcola valori derivati
		full_name = first_name + " " + last_name;
		number_of_books = books.length;
	}
}

La possibilità di deserializzare oggetti arbitrari permette di sfruttare determinate classi la cui implementazione del metodo readObject, eseguito con input malevoli, può portare ad attacchi al sistema. Chiaramente, l'entità dell'attacco deriva dall'implementazione del metodo readObject e quindi da tutte le classi caricate. Bisogna anche tenere presente che non sempre l'attacco coinvolge una sola classe, ma potrebbe anzi sfruttare diversi "gadget" concatenati. Esistono strumenti appositi, il più famoso è ysoserial, in grado di generare appositi payload che sfruttino la presenza di determinate "gadget chain".

È importante notare come la vulnerabilità non sia dovuta dall'implementazione del metodo readObject, in quanto questo dipende esclusivamente dalla logica di business, ma invece dalla possibilità di un utente di deserializzare oggetti arbitrari. Per questo, quando si fa uso di deserializzazione, è di fondamentale importanza assicurarsi che i dati in input siano di tipi ben definiti, facenti uso di appropriate mitigazioni.

.NET

In .NET gli attacchi si svolgono in maniera simile a Java, sfruttando opportune classi gadget che eseguono specifico codice in fase di deserializzazione.

In questo caso si può utilizzare lo strumento ysoserial.net, basato sulla stessa idea di ysoserial, ma specifico per il framework .NET.

È bene ricordare che la possibilità di sfruttare uno specifico payload dipende dalla presenza del relativo gadget all'interno del codice sorgente. Alcuni payload richiedono quindi determinate configurazioni o l'utilizzo di determinate librerie.

PHP

Anche PHP offre un meccanismo di serializzazione e deserializzazione nativo. Durante i processi di serializzazione e deserializzazione, vengono utilizzati particolari "metodi magici" (magic methods) che possono essere ridefiniti per ogni classe:

  • __sleep: invocato quando un oggetto viene serializzato, deve ritornare una lista contenente i nomi di tutte le proprietà dell'oggetto che si desidera serializzare
  • __wakeup: invocato quando un oggetto viene deserializzato, generalmente utilizzato per reinizializzare stati interni, come potrebbe essere una connessione a un database
  • __unserialize: se presente invocato al posto di __wakeup, a differenza di quest'ultimo offre un maggiore controllo
  • __destruct: invocato quando un oggetto viene distrutto o lo script finisce
  • __toString: invocato quando un oggetto viene trattato come stringa

Come nei casi di Java e .NET, è possibile cercare all'interno del sorgente dell'applicativo sotto esame, o delle sue librerie, classi che implementino queste funzioni e che possano essere usate come gadget. Anche in questo caso esistono strumenti per la generazione dei payload di attacco, come ad esempio PHPGGC.

Python

Python include una famosa libreria di serializzazione/deserializzazione chiamata pickle. A differenza degli esempi precedenti, pickle permette di ottenere esecuzione di codice arbitrario (RCE) senza alcun prerequisito.

Ciò è possibile perché in Python pickle permette anche di serializzare e deserializzare funzioni, incluso il metodo __reduce__() utilizzato dalle classi in fase di deserializzazione.

Generare un payload RCE per pickle è immediato:

import pickle
import os

class RCE:
	def __reduce__(self):
		return os.system, ('echo pwned',)

payload = pickle.dumps(RCE())

il valore serializzato della classe RCE include l'implementazione del metodo __reduce__, e per questo porta all'esecuzione di codice arbitrario su qualsiasi sitema deserializzi il nostro payload.