Object injection : principes, exploitations et bonnes pratiques sécurité

L’injection d’objet (object injection) est une vulnérabilité applicative qui survient lorsqu’une application désérialise des données non fiables.

Si un attaquant parvient à injecter un objet malveillant, il peut en exploiter les propriétés pour exécuter du code arbitraire, voler des données, modifier le comportement de l’application ou encore manipuler des fichiers à distance. En d’autres termes, cette faille peut entraîner une compromission totale du système ciblé.

Tous les langages prenant en charge la désérialisation d’objets sont potentiellement vulnérables, mais PHP est particulièrement exposé. Dans cet article, nous reviendrons sur les principes de la sérialisation pour expliquer le principe de l’object injection. Nous examinerons également des exemples d’exploitation ainsi que les méthodes pour prévenir l’injection d’objets.

Guide complet sur l’object injection

Comprendre les principes de la sérialisation et de la désérialisation

Pour comprendre l’injection d’objet, il est essentiel de saisir le concept de sérialisation et son utilité.

La sérialisation permet de convertir un objet en une chaîne de texte afin de le stocker ou de l’échanger entre applications. Contrairement aux données classiques comme le texte ou les fichiers, un objet est plus complexe : il existe en mémoire, possède des attributs et méthodes spécifiques, et peut évoluer au cours de son cycle de vie. Grâce à la sérialisation, ces caractéristiques sont préservées sous une forme lisible et transmissible.

Lorsqu’une application reçoit cette chaîne, elle peut la désérialiser pour recréer un objet identique à l’original. Ce mécanisme est très pratique pour le partage de données entre systèmes, mais il peut aussi être détourné à des fins malveillantes, comme nous allons le voir.

Prenons l’exemple de deux applications partageant le même disque et souhaitant échanger un objet A, défini comme suit :

L’application1 sérialise l’objet et l’enregistre dans un fichier « store ».

L’application 2 récupère ensuite ce fichier, le désérialise et recrée ainsi l’objet d’origine. Cependant, pour que cette opération fonctionne, elle doit connaître la définition de la classe de l’objet (via un fichier comme A.php).

L’objet A sera sérialisé sous la forme suivante :

O:1:"A":1:{s:10:"objectData";s:6:"blabla";}

Décomposons cette structure :

  • “O” indique qu’il s’agit d’un objet.
  • 1:"A" signifie que le nom de la classe contient 1 caractère et s’appelle A.
  • 1:{…} indique que l’objet possède 1 attribut.
  • s:10:"objectData" précise que l’attribut est une chaîne (s pour string), de 10 caractères, nommée objectData.
  • s:6:"blabla" montre que cet attribut contient une chaîne de 6 caractères, soit "blabla".

Il est important de noter que les propriétés statiques, y compris les méthodes, ne sont pas incluses dans la sérialisation. C’est pourquoi l’application 2 doit impérativement connaître la classe A pour reconstruire l’objet.

Qu’est-ce qu’une faille object injection ?

L’injection d’objet est une vulnérabilité qui exploite la désérialisation d’un objet malveillant. En injectant des attributs modifiés, un attaquant peut provoquer des comportements imprévus dans l’application.

Toutefois, la désérialisation n’inclut pas les méthodes statiques (comme les fonctions). L’attaquant ne peut donc pas simplement ajouter du code malveillant dans une fonction et espérer qu’elle s’exécute. À la place, il doit exploiter des attributs non statiques que l’application utilise de manière non sécurisée.

Le problème vient souvent du fait que l’application fait aveuglément confiance aux attributs protégés ou privés, ou suppose qu’un objet sérialisé ne peut pas être modifié – ce qui est faux.

Un des vecteurs d’attaque les plus courants repose sur l’injection d’objets exploitant des fonctions « magiques ». Ces fonctions, propres à certains langages, sont automatiquement exécutées à différentes étapes du cycle de vie de l’objet.

Exemples de fonctions magiques en PHP :

  • __construct() → appelé lors de l’initialisation de l’objet
  • __destruct() → exécuté lors de la suppression de l’objet
  • __wakeup() → utilisé lors de la désérialisation
  • __toString() → déclenché lorsqu’un objet est traité comme une chaîne
  • __invoke() → exécuté si l’objet est appelé comme une fonction

En exploitant ces mécanismes, un attaquant s’assure que son code sera exécuté tôt ou tard, car l’application finira par appeler l’une de ces fonctions.

Exemples d’exploitation de failles object injection

L’injection d’objet peut être exploitée de plusieurs manières, notamment pour exécuter du code arbitraire ou contourner des mécanismes de sécurité. Voici deux scénarios concrets illustrant ces attaques.

Prenons une application vulnérable où l’objet sérialisé est transmis via un paramètre GET :

L’objet A possède maintenant un attribut privé internal et une fonction magique __destruct() :

Ici, la fonction __destruct() est une vulnérabilité critique : elle exécute arbitrairement le contenu de l’attribut privé internal via eval().

Cet attribut étant privé, il ne peut pas être modifié directement, mais un attaquant peut l’altérer en exploitant la désérialisation.

Objet A sérialisé normalement :

O:1:"A":2:{s:10:"objectData";s:6:"blabla";s:11:"Ainternal";s:12:"echo 'test';";}

Détail du format :

  • O:1:"A" → Objet de classe A (nom de 1 caractère).
  • s:10:"objectData" → Attribut public objectData contenant "blabla".
  • s:11:"Ainternal" → Attribut privé internal, préfixé par A dans la sérialisation.

Un attaquant peut donc injecter une charge malveillante dans internal en envoyant cette requête :

https://application2.com?input=O:1:"A":2:{s:10:"objectData";s:6:"blabla";s:11:"Ainternal";s:10:"phpinfo();";}

Lors de la désérialisation, internal est défini sur "phpinfo();", ce qui entraîne son exécution lorsque __destruct() est appelé :

eval("phpinfo();");

Ce simple exemple exécute phpinfo(), mais un attaquant pourrait injecter n’importe quel code PHP, menant à une compromission complète du serveur.

Dans certains cas, l’exploitation d’une object injection est plus subtile. Une désérialisation mal sécurisée peut être utilisée indirectement pour injecter du code malveillant ailleurs dans l’application.

Ici, l’objet A contient un attribut privé username. L’application utilise ensuite cet objet pour construire une requête SQL.

L’attribut username est utilisé directement dans une requête SQL sans protection, rendant l’application vulnérable à une injection SQL.

Un attaquant peut exploiter cette faille en envoyant la requête suivante :

https://application2.com?input=O:1:"A":1:{s:8:"username";s:15:"test1' OR 1=1--";}

Ce qui est interprété ainsi par l’application :

SELECT * FROM users WHERE username = 'test1' OR 1=1--

L’opérateur OR 1=1-- transforme la requête en une requête toujours vraie, ce qui entraîne l’affichage de tous les utilisateurs de la base de données.

Ces exemples montrent comment une faille d’object injection peut être exploitée pour :

  • Exécuter du code arbitraire via des fonctions magiques (__destruct(), __wakeup(), etc.).
  • Contourner des protections et injecter du code malveillant dans d’autres parties de l’application (comme une requête SQL).

Dans des applications réelles, ces vulnérabilités peuvent être plus difficiles à détecter, notamment si le code source n’est pas accessible. Une analyse approfondie est donc nécessaire pour identifier ces failles et éviter qu’elles ne soient exploitées.

Comment prévenir les failles object injection ?

Les attaques basées sur l’object injection démontrent que désérialiser des objets provenant d’une source non fiable est extrêmement risqué. Si ce mécanisme est mal maîtrisé, il peut mener à des compromissions critiques, allant de l’exécution de code arbitraire à des injections SQL.

Mais alors, comment se prémunir contre ces attaques ?

Avant d’implémenter la sérialisation/désérialisation, il est essentiel de se poser une question fondamentale : Avons-nous vraiment besoin de désérialiser des objets ?

Dans de nombreux cas, une approche plus sécurisée peut être envisagée, comme :

  • Utiliser des formats de données standardisés comme JSON ou XML, avec un parsing strict.
  • Stocker uniquement les données nécessaires plutôt que des objets complets.

Si la désérialisation est inutile ou évitable, il est préférable de ne pas l’utiliser du tout.

Si la désérialisation est réellement requise, il est impératif de valider chaque donnée reçue :

  • Utiliser des whitelists pour restreindre les classes pouvant être désérialisées.
  • Vérifier les types et formats des données avant toute utilisation.
  • Implémenter un mécanisme de validation stricte pour les attributs désérialisés.

En PHP, l’option allowed_classes de unserialize() permet d’empêcher la désérialisation de classes arbitraires :

$data = unserialize($_GET['input'], ["allowed_classes" => false]); // Désérialisation uniquement des types natifs

Si vous devez désérialiser des objets, définissez explicitement quelles classes sont autorisées :

$data = unserialize($_GET['input'], ["allowed_classes" => ["MaClasseSecurisee"]]);

Les fonctions magiques (__wakeup(), __destruct(), etc.) sont les premières cibles des attaquants. Il est préférable d’éviter d’implémenter ces méthodes dans des classes susceptibles d’être désérialisées.

Une erreur courante est de croire qu’un objet désérialisé est « sûr ».
Il faut plutôt le considérer comme toute autre entrée utilisateur potentiellement malveillante.

Appliquez les bonnes pratiques de sécurité classiques :

  • Ne jamais utiliser eval() avec des données désérialisées.
  • Échapper correctement les données avant toute utilisation (SQL, fichiers, commandes, etc.).
  • Restreindre les accès et permissions des classes instanciées.

Auteur : Renaud CAYOL – Pentester @Vaadata