Désérialisation PHAR dans l'écosystème WordPress : d'un upload anodin à l'exécution de code à distance
L'injection d'objets PHP est souvent présentée comme une vulnérabilité nécessitant un appel direct à unserialize(). En réalité, les applications PHP modernes exposent une seconde surface de désérialisation plus discrète : le wrapper de flux PHAR. Lorsqu'une application effectue presque n'importe quelle opération de système de fichiers sur un chemin phar://, PHP analyse les métadonnées de l'archive et les désérialise. Aucun appel explicite à unserialize() n'est nécessaire. Cet article explique le mécanisme, examine la vulnérabilité WordPress CVE-2025-2105 et montre comment les défenseurs peuvent localiser et éliminer le point d'entrée.
1. L'injection d'objets en un paragraphe
La sérialisation PHP convertit un objet en un flux d'octets qui enregistre le nom de la classe et ses propriétés. La désérialisation reconstruit cet objet. Si un attaquant contrôle le flux, il contrôle le nom de la classe et chaque valeur de propriété. Le danger ne vient pas de la reconstruction elle-même, mais des méthodes magiques que PHP invoque automatiquement : __wakeup(), __destruct(), __toString(), etc. Une chaîne de ces méthodes à travers des classes légitimes — une chaîne de gadgets POP — peut transformer un graphe d'objets forgé en écriture de fichier, requête SQL ou exécution de commande.
CVE-2025-2105 en est un exemple typique. Elle affectait le plugin Jupiter X Core pour WordPress jusqu'à la version 4.8.11 et a été corrigée en 4.8.12. L'avis la classe comme injection d'objets PHP non authentifiée (CWE-502) avec un score CVSS de 8.1. Le nom du plugin importe peu : une requête non authentifiée pouvait atteindre une primitive de désérialisation dans un composant que beaucoup de propriétaires de sites n'auditent jamais.
2. Pourquoi PHAR change le modèle de menace
Une archive PHAR est un format de paquet propre à PHP. Elle contient un stub, un manifeste et le contenu des fichiers. Le manifeste peut stocker des métadonnées sérialisées. D'après la documentation PHP, le wrapper phar:// analyse ces métadonnées à l'ouverture de l'archive. La désérialisation est un effet de bord d'un appel de système de fichiers normal :
// Chacun de ces appels peut déclencher la désérialisation des métadonnées PHAR
file_exists('phar:///var/www/uploads/avatar.jpg');
filesize('phar:///var/www/uploads/avatar.jpg');
getimagesize('phar:///var/www/uploads/avatar.jpg');
file_get_contents('phar:///var/www/uploads/avatar.jpg');
Conséquence pratique : une application qui accepte un upload puis appelle une fonction de système de fichiers sur un chemin influencé par l'utilisateur peut être exploitable même sans appel à unserialize(). Le fichier n'a pas besoin de l'extension .phar. Il suffit qu'il soit une archive PHAR valide que l'application ouvre via le wrapper.
Les uploads polyglottes
Une archive PHAR peut ressembler à une image. Un fichier peut commencer par un en-tête JPEG ou GIF valide et contenir un manifeste PHAR plus loin. Une validation qui ne vérifie que les premiers octets, le type MIME annoncé par le navigateur ou l'extension est donc insuffisante. Le serveur ne doit jamais construire un chemin phar:// à partir d'une entrée utilisateur.
3. Construire un PHAR minimal en laboratoire
Le code suivant est réservé à un environnement de laboratoire isolé. Il montre comment les métadonnées sont intégrées. La création d'un PHAR exige phar.readonly=0 ; les serveurs de production doivent conserver la valeur par défaut phar.readonly=1.
<?php
// lab-build-phar.php — exécuter avec : php -d phar.readonly=0 lab-build-phar.php
class DemoGadget {
public $cmd = 'id';
public function __destruct() {
// Puits volontairement trivial, uniquement pour la démonstration.
if (is_string($this->cmd)) {
system($this->cmd);
}
}
}
@unlink('/tmp/lab.phar');
$p = new Phar('/tmp/lab.phar');
$p->startBuffering();
$p->addFromString('x.txt', 'lab');
$p->setStub("<?php __HALT_COMPILER(); ?>");
$p->setMetadata(new DemoGadget());
$p->stopBuffering();
echo "PHAR écrit\n";
Côté vulnérable, un seul appel suffit à instancier le graphe d'objets :
<?php
// lab-trigger.php
file_exists('phar:///tmp/lab.phar/x.txt');
La sortie de id apparaît à la destruction de l'objet. Lors d'un audit réel, la classe gadget serait une classe existante de l'application ou d'une bibliothèque embarquée, et l'objectif serait une primitive d'écriture ou d'exécution sans classe personnalisée.
4. Le schéma CVE-2025-2105
L'incident Jupiter X Core illustre la même erreur architecturale au niveau applicatif : une entrée non fiable atteignait un point de désérialisation. Trois conditions le rendaient grave :
- Aucune authentification requise. Le chemin vulnérable était accessible avant connexion, ce qui supprime la précondition d'exploitation la plus courante.
- Un vaste écosystème de plugins. Les plugins WordPress embarquent de nombreuses classes. Même sans gadget utile dans le plugin vulnérable, un site avec des dizaines de plugins peut contenir une chaîne complète.
- Adoption silencieuse du correctif. Les utilisateurs ayant désactivé les mises à jour automatiques ou épinglé une ancienne version restaient exposés.
La CVE rappelle que l'injection d'objets doit être traitée comme une classe d'exécution de code à distance, pas comme une simple divulgation d'information. Le score CVSS 8.1 reflète cette distinction.
5. Localiser les points de désérialisation lors d'une revue de code
Commencez par les points directs, puis élargissez aux fonctions de système de fichiers pouvant recevoir un chemin phar:// :
# Désérialisation directe
grep -rn --include='*.php' -E '\b(unserialize|maybe_unserialize)\s*\(' .
# Appels de fichiers compatibles PHAR
grep -rn --include='*.php' -E '\b(file_exists|filesize|getimagesize|file_get_contents|file_put_contents|is_file|is_dir|stat|md5_file|sha1_file|copy|rename|unlink)\s*\(' .
# Gestionnaires d'upload et construction de chemins
grep -rn --include='*.php' -E '(move_uploaded_file|wp_handle_upload|\$_FILES)' .
Pour chaque résultat, remontez l'argument. Si une partie du chemin est contrôlée par l'utilisateur et que le code peut être amené à préfixer phar://, le point est dangereux. Surveillez particulièrement le traitement d'images, les gestionnaires d'avatars, l'import/export et tout code qui valide un fichier puis le rouvre par son chemin.
Vérification à l'exécution
En laboratoire, activez phar.readonly=1, désactivez le wrapper uniquement si votre plan de test l'exige, et placez un point d'arrêt Xdebug sur Phar::getMetadata(). Si les métadonnées sont lues, le graphe d'objets a déjà été instancié. C'est le moment de confirmer quelles classes sont disponibles et si une chaîne existe.
6. Contrôles défensifs réellement efficaces
- Ne jamais construire un chemin
phar://à partir d'une entrée. Résolvez les uploads dans un répertoire fixe et utilisez un nom généré par le serveur. Rejetez tout chemin contenant un schéma de wrapper. - Validez le contenu, pas l'étiquette. Utilisez
finfoougetimagesize()sur le vrai fichier temporaire, ré-encodez les images et supprimez les métadonnées. Un en-tête d'image valide ne prouve pas l'absence de manifeste PHAR. - Conservez
phar.readonly=1. Cela empêche la création, pas l'analyse : c'est un durcissement, pas un correctif complet. - Désactivez les wrappers inutilisés. Si l'application n'a pas besoin de PHAR, retirez le wrapper de la liste autorisée et appliquez-le au niveau du serveur web.
- Corrigez rapidement et inventoriez les plugins. CVE-2025-2105 a été corrigée dans Jupiter X Core 4.8.12. Maintenez une nomenclature logicielle des composants WordPress et supprimez les plugins abandonnés.
- Appliquez le moindre privilège au compte web. Un processus PHP incapable d'écrire hors de son répertoire d'upload et de lancer un shell limite l'impact de toute chaîne de gadgets.
7. Conclusion
La désérialisation PHAR rappelle que les frontières de sécurité sont définies par les flux de données, pas par les noms de fonctions. Une application peut être vulnérable sans jamais appeler unserialize() ; un fichier qui ressemble à un JPEG peut être un graphe d'objets sérialisé. CVE-2025-2105 montre à quelle vitesse une primitive d'injection d'objets non authentifiée dans un plugin WordPress largement déployé devient un risque d'exécution de code. Les défenseurs doivent considérer chaque appel de système de fichiers sur un chemin influencé par l'utilisateur comme un point de désérialisation potentiel, et chaque upload comme du code non fiable jusqu'à son ré-encodage et son stockage sous un nom contrôlé par le serveur.