Aller au contenu

Désérialisation non sécurisée

La sérialisation est le processus qui convertit une structure de données complexe — un objet et ses attributs — en un format plus simple, transmissible comme un flux d'octets. Elle sert à écrire des données complexes sur disque ou en base, et à les échanger sur un réseau ou via une API. Point essentiel : l'état de l'objet est préservé, c'est-à-dire que ses attributs et leurs valeurs sont conservés tels quels.

La désérialisation est l'opération inverse : elle reconstitue, à partir du flux d'octets, une réplique fonctionnelle de l'objet d'origine, dans l'état exact où il se trouvait. L'application peut alors manipuler cet objet reconstitué comme n'importe quel autre.

La désérialisation devient non sécurisée lorsque l'application désérialise des données contrôlables par l'utilisateur sans en vérifier l'intégrité. Selon le langage, la sérialisation porte des noms différents — marshalling en Ruby, pickling en Python — mais le principe et le risque sont identiques.

Identifier une désérialisation

Repérer une désérialisation non sécurisée est relativement simple, en boîte blanche comme en boîte noire : il s'agit d'examiner toutes les données transmises à l'application et d'y reconnaître des objets sérialisés, puis de vérifier qu'on peut les contrôler.

En PHP, le format est une chaîne généralement lisible, où des lettres indiquent le type et des nombres la longueur. Pour un objet User doté de deux attributs :

$user->name = "exemple";
$user->isLoggedIn = true;

la forme sérialisée ressemble à :

O:4:"User":2:{s:4:"name":s:7:"exemple";s:10:"isLoggedIn";b:1;}

qui se lit ainsi : O:4:"User" est un objet dont le nom de classe fait 4 caractères ; 2 indique deux attributs ; s:4:"name" est une clé de type chaîne de 4 caractères ; s:7:"exemple" la valeur associée ; b:1 un booléen vrai. Les fonctions natives sont serialize() et unserialize() — en boîte blanche, on cherche les appels à unserialize().

En Java, le format est binaire, donc moins lisible, mais reconnaissable à quelques marqueurs : un objet sérialisé commence toujours par les octets ac ed en hexadécimal, soit rO0 en Base64. Toute classe implémentant java.io.Serializable peut être désérialisée ; en boîte blanche, on repère les appels à readObject().

Détection automatique

Burp Scanner signale automatiquement les messages HTTP qui semblent contenir des objets sérialisés, ce qui accélère le repérage initial.

Manipuler les attributs d'un objet

L'exploitation la plus simple consiste à modifier directement la valeur d'un attribut. L'état étant préservé, on analyse l'objet sérialisé, on repère un attribut sensible et on en modifie la valeur avant de réinjecter l'objet. Considérons un site qui stocke la session dans un cookie sous forme d'objet User sérialisé :

O:4:"User":2:{s:8:"username";s:7:"exemple";s:7:"isAdmin";b:0;}

L'attribut isAdmin est manifestement intéressant. En passant sa valeur booléenne à 1, puis en réencodant l'objet, on tente une élévation de privilèges. Cela ne fonctionne que parce que le code vulnérable fait confiance à l'objet sans vérifier son authenticité :

$user = unserialize($_COOKIE);
if ($user->isAdmin === true) {
    // accès à l'interface d'administration
}

En pratique, ces objets transitent souvent encodés en Base64 (et parfois en URL par-dessus) : on décode l'URL puis le Base64, on modifie l'attribut, et on réencode dans l'ordre inverse en veillant à conserver une éventuelle terminaison = correctement encodée.

Manipuler les types de données

On peut aussi fournir un type inattendu. La logique PHP y est particulièrement sensible à cause de l'opérateur de comparaison lâche ==, qui convertit les types avant de comparer. Sur PHP 7 et antérieur, 0 == "chaîne quelconque" est évalué à vrai, car la chaîne est convertie en l'entier 0. Couplé à une donnée désérialisée, ce comportement crée des failles logiques :

$login = unserialize($_COOKIE);
if ($login['password'] == $password) {
    // authentification réussie
}

En remplaçant la valeur du mot de passe par l'entier 0 (et non une chaîne), la condition devient toujours vraie tant que le mot de passe stocké ne commence pas par un chiffre. Cela n'est possible que parce que la désérialisation préserve le type :

{s:8:"username";s:7:"exemple";s:12:"access_token";i:0;}

Cohérence des étiquettes

En modifiant un type, il faut aussi mettre à jour l'étiquette de type et l'indicateur de longueur, sinon l'objet est corrompu et ne se désérialise pas. Sur PHP 8, les chaînes ne sont plus implicitement converties en 0, ce qui neutralise ce cas précis. L'extension Hackvertor ajuste automatiquement les décalages des formats binaires et fait gagner un temps précieux.

Exploiter les fonctionnalités de l'application

Au-delà de la vérification d'attributs, certaines fonctionnalités effectuent des opérations dangereuses sur les données issues de l'objet désérialisé. Si une fonction « Supprimer mon compte » efface la photo de profil en lisant le chemin stocké dans $user->image_location, un objet modifié pointant vers un chemin arbitraire conduit à la suppression de ce fichier :

O:4:"User":3:{...;s:11:"avatar_link";s:18:"/chemin/vers/cible";}

Ce cas suppose une invocation manuelle de la méthode dangereuse. La désérialisation devient bien plus puissante lorsqu'on déclenche ces opérations automatiquement, grâce aux méthodes magiques.

Méthodes magiques

Les méthodes magiques sont un sous-ensemble de méthodes qu'il n'est pas nécessaire d'appeler explicitement : elles sont déclenchées automatiquement lorsqu'un événement survient. Elles sont courantes en programmation orientée objet et souvent signalées par un préfixe particulier (un double tiret bas en PHP). Le constructeur __construct() en est un exemple, appelé à chaque instanciation.

Elles ne constituent pas une vulnérabilité en soi, mais le deviennent quand le code qu'elles exécutent manipule des données contrôlables par l'attaquant. Surtout, certaines sont invoquées automatiquement lors de la désérialisation : unserialize() en PHP recherche et appelle __wakeup(), et une classe Java peut déclarer sa propre méthode readObject() qui agit alors comme une méthode magique :

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
    // implémentation exécutée à la désérialisation
}

Ces méthodes transmettent des données de l'objet sérialisé au code de l'application avant même la fin de la désérialisation : c'est le point de départ des exploits plus sophistiqués.

Injecter un objet d'une autre classe

Les méthodes de désérialisation ne vérifient généralement pas le type de l'objet qu'elles reconstituent. On peut donc fournir un objet de n'importe quelle classe sérialisable présente dans l'application. Même si cet objet n'est pas celui attendu et provoque ensuite une exception, il aura déjà été instancié — et sa méthode magique, déclenchée.

En boîte blanche, on parcourt les classes disponibles à la recherche de méthodes magiques de désérialisation qui effectuent une opération dangereuse sur des données accessibles. Prenons une classe PHP comportant un destructeur :

<?php
class CustomTemplate {
    private $template_file_path;
    private $lock_file_path;

    public function __construct($template_file_path) {
        $this->template_file_path = $template_file_path;
        $this->lock_file_path = $template_file_path . ".lock";
    }

    function __destruct() {
        if (file_exists($this->lock_file_path)) {
            unlink($this->lock_file_path);
        }
    }
}
?>

__destruct() est une méthode magique, appelée automatiquement à la fin de vie de l'objet ou à la fin du script. En injectant un objet CustomTemplate sérialisé dont l'attribut lock_file_path pointe vers un fichier cible, on provoque sa suppression :

O:14:"CustomTemplate":1:{s:14:"lock_file_path";s:18:"/chemin/vers/cible";}

Lire le code source

Il est parfois possible de récupérer un fichier de sauvegarde laissé par un éditeur en ajoutant un tilde au nom de fichier : GET /libs/CustomTemplate.php~. Cela donne accès au code source nécessaire pour construire l'objet.

Chaînes de gadgets

Un gadget est un fragment de code déjà présent dans l'application qui aide à atteindre un objectif. Pris isolément, un gadget n'est pas malveillant, mais en enchaînant plusieurs gadgets — chacun transmettant ses données au suivant — on peut acheminer une donnée contrôlée jusqu'à un gadget récepteur dangereux.

Point fondamental : contrairement à d'autres exploits, une chaîne de gadgets n'est pas une suite de méthodes écrite par l'attaquant. Tout le code existe déjà dans l'application ; l'attaquant ne contrôle que les données qui transitent dans la chaîne, généralement amorcée par une méthode magique de désérialisation. La capacité à construire ces chaînes est l'élément clé de l'exploitation avancée.

Chaînes pré-construites

Identifier une chaîne manuellement est fastidieux, voire impossible sans le code source. Heureusement, des bibliothèques largement répandues contiennent des chaînes déjà connues : si une chaîne fonctionne sur une bibliothèque donnée, elle fonctionne sur tout site qui l'utilise.

ysoserial est l'outil de référence pour Java : on choisit une chaîne correspondant à une bibliothèque supposée présente, on fournit la commande à exécuter, et l'outil génère l'objet sérialisé. À partir de Java 16, des arguments supplémentaires sont nécessaires pour l'exécuter :

java \
  --add-opens=java.xml/com.sun.org.apache.xalan.internal.xsltc.trax=ALL-UNNAMED \
  --add-opens=java.base/java.net=ALL-UNNAMED \
  --add-opens=java.base/java.util=ALL-UNNAMED \
  -jar ysoserial-all.jar CommonsCollections4 'id' | base64

Le payload obtenu s'injecte au bon endroit (souvent le cookie de session) et se rejoue via Burp Repeater, en veillant à la cohérence d'une éventuelle terminaison =.

Deux chaînes universelles servent à la détection, indépendamment de toute bibliothèque vulnérable :

  • URLDNS déclenche une résolution DNS vers une URL fournie. Elle fonctionne sur toutes les versions de Java et constitue la sonde la plus universelle : une interaction DNS reçue confirme qu'une désérialisation a eu lieu.
  • JRMPClient force une connexion TCP vers une adresse IP brute. Utile quand le trafic sortant, DNS compris, est filtré : si l'application répond immédiatement avec une IP locale mais se bloque avec une IP externe filtrée, la différence de délai trahit la désérialisation.

Pour PHP, l'équivalent est PHPGGC (PHP Generic Gadget Chains) :

./phpggc Symfony/RCE4 exec 'id' | base64

Lorsque l'application authentifie ses cookies via une clé secrète, il faut souvent récupérer cette clé (par exemple via une fuite d'information exposant les variables d'environnement) puis signer l'objet généré. Un court script PHP suffit à produire un cookie signé valide :

<?php
$object = "OBJET-GÉNÉRÉ-PAR-PHPGGC";
$secretKey = "CLÉ-RÉCUPÉRÉE";
$cookie = urlencode('{"token":"' . $object . '","sig_hmac_sha1":"' . hash_hmac('sha1', $object, $secretKey) . '"}');
echo $cookie;

La vulnérabilité n'est pas le gadget

La faille réside dans la désérialisation de données non fiables, pas dans la simple présence d'une chaîne de gadgets dans le code. La chaîne n'est qu'un moyen d'acheminer les données malveillantes : un site reste vulnérable même s'il parvient à purger toutes les chaînes connues.

Chaînes documentées

Quand aucun outil dédié n'existe pour le framework cible, il vaut la peine de chercher en ligne des exploits documentés à adapter. Cela demande une certaine compréhension du langage et du framework, et parfois de sérialiser l'objet soi-même, mais reste bien moins coûteux que de partir de zéro. Pour Ruby, par exemple, une chaîne connue exploitant les classes internes de Gem permet d'exécuter une commande système ; elle peut nécessiter un environnement d'une version précise de Ruby pour fonctionner, ce qu'un conteneur Docker permet d'isoler facilement.

Construire sa propre chaîne

En dernier recours, il faut développer sa propre chaîne, ce qui demande presque toujours l'accès au code source. La démarche : repérer une classe dont une méthode magique est invoquée à la désérialisation, analyser le code qu'elle exécute, et vérifier si elle manipule dangereusement des attributs contrôlables. Si la méthode magique n'est pas exploitable seule, elle sert de point de départ : on examine les méthodes qu'elle appelle, puis les leurs, en notant à chaque étape les valeurs accessibles, jusqu'à atteindre un sink dangereux. On construit ensuite un objet sérialisé valide portant les valeurs requises.

Désérialisation PHAR

En PHP, on peut parfois déclencher une désérialisation sans appel explicite à unserialize(). Le wrapper d'URL phar:// donne accès aux archives PHP (.phar), dont le manifeste contient des métadonnées sérialisées. Or toute opération de système de fichiers sur un flux phar:// désérialise implicitement ces métadonnées.

Des fonctions peu suspectes comme file_exists() sont souvent moins protégées que include() ou fopen(). La technique demande de déposer le fichier PHAR sur le serveur ; en créant un fichier polyglotte — un contenu PHAR déguisé en image JPEG — on contourne fréquemment la validation d'upload, l'extension n'étant pas vérifiée lors de la lecture du flux. Si la classe de l'objet est prise en charge, __wakeup() et __destruct() peuvent alors être invoquées, ouvrant la voie à une chaîne de gadgets.

Aide-mémoire

Étape Approche
Identifier Format PHP lisible (O:), Java binaire (ac ed / rO0) ; Burp Scanner
Exploitation simple Modifier un attribut sensible (isAdmin)
Faille logique Modifier le type (entier 0 et comparaison lâche PHP 7)
Déclenchement automatique Méthode magique (__wakeup, __destruct, readObject)
Détection universelle Chaînes ysoserial URLDNS ou JRMPClient
Exploitation Java / PHP ysoserial / PHPGGC avec chaîne adaptée à une bibliothèque
Sans unserialize() explicite Désérialisation via flux phar:// et fichier polyglotte

La construction de chaînes de gadgets étant le cœur du sujet, la documentation des frameworks ciblés et les dépôts d'exploits publics restent des ressources de premier plan.