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’appelleA
.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éeobjectData
.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.
Exploitation d’une object injection via exécution de code arbitraire
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 classeA
(nom de 1 caractère).s:10:"objectData"
→ Attribut publicobjectData
contenant"blabla"
.s:11:"Ainternal"
→ Attribut privéinternal
, préfixé parA
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.
Exploitation d’une object injection via injection SQL
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 ?
Éviter la désérialisation quand ce n’est pas strictement nécessaire
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.
Vérifier et filtrer les données reçues
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"]]);
Éviter les fonctions magiques dans les objets sérialisables
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.
Toujours considérer les objets désérialisés comme des entrées utilisateur
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