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:
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:
def insertPayload(_in, _out, payload,off):
img = _in
# look for 'FF DA' (SOS)
sos = img.index("\xFF\xDA")
sos_size = struct.unpack('>H',img[sos+2:sos+4])[0]
sod = sos_size+2
# look for 'FF D9' (EOI)
eoi = img[sod:].index("\xFF\xD9")
# enough size ?
if (eoi - sod - off)>=len(payload):
_out.write(img[:sod+sos+off]+payload+img[sod+sos+len(payload)+off:])
return True
else:
return False
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:
<?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:
version de GD employée: 2.0.36rc1
version de Python employée: 2.7
Durant le meeting du mois de Janvier, j'avais présenté un de mes outils nommé Vodstok (pour Voluntary Distributed Storage Kit), mais il n'était pas finalisé. J'ai pris un peu de temps parmi le peu disponible pour tenter de finaliser le code et pouvoir le mettre à disposition de tous. Cet outil permet à tout un chacun de pouvoir uploader et télécharger des fichier stockés sur Internet, dans un nuage de serveurs de stockage mis à disposition par des volontaires.
Stockage distribué volontaire
Des personnes utilisant Vodstok l'installent sur leurs serveurs web (aucune base de données requise, compatible PHP4/5) et décident ainsi de partager une portion de leur espace de stockage aux utilisateurs de Vodstok. Chaque propriétaire de serveur peut ainsi s'ajouter à un réseau maillé de serveurs, et l'ensemble constitue une plate-forme de stockage distribué accessible en HTTP.
La force de ce système est d'une part sa résistance aux coupures: si un serveur tombe, le système est toujours fonctionnel, bien que les données du serveur perdu soient considérées comme perdues (Vodstok n'assure pas de redondance actuellement). Mais il est toujours possible de s'en servir, et en comptant sur l'effet Streisand, cela devrait a priori favoriser ce système. D'autre part, Vodstok se base sur le protocole HTTP, qui est universel et permet même de passer au travers de proxies, ce qui évite d'avoir une configuration de ports particulière, comme c'est le cas avec BitTorrent par exemple.
Vodstok intègre un client en ligne de commande écrit en Python, qui permet d'assurer un stockage sécurisé (AES256) et une répartition d'un fichier sur l'ensemble des serveurs. Bien sûr ce client peut être modifié et les usages dérivés sont nombreux, la seule limite reste votre imagination. Pour faire simple, un serveur sur lequel vodstok est déployé fait office de système de stockage seul, il n'y a pas d'autre logique particulière. On peut le considérer comme un "bout de disque dur" qui ne regarde pas ce qui y est stocké ni comment s'en servent les utilisateurs.
Comment partager une partie de son espace de stockage web avec Vodstok
En premier lieu, il faut récupérer l'archive de Vodstok sur la page du projet, et copier le contenu du dossier www sur le serveur distant. Une rapide modification du fichier de configuration config.inc.php permet de paramétrer le bouzin:
<?php
define('QUOTA_MB', 1024); /* Je décide de partager 1Go d'espace disque */
define('MAX_ENDPOINTS',1500);
define('MAX_ENDPOINT_LEN',200);
/*
DO NOT MODIFY OR REMOVE THE FOLLOWING LINES
*/
define('QUOTA',QUOTA_MB*1024*1024);
define('CHUNK_DIR','chunks');
define('ENDPOINT_DIR','endpoints');
?>
On chmod 777 les dossiers chunks et endpoints, et c'est tout, Vodstok est paré à fonctionner côté serveur. Reste à voir comment s'en servir côté client.
Pour le client, rien de bien compliqué: le dossier client contient le code source de l'outil, qui est pour l'instant seulement en ligne de commande. On le copie dans un dossier accessible à l'utilisateur, et on peut le lancer:
# chmod +x /opt/vodstok-1.2 && cd /usr/bin/local && ln -s /opt/vodstok-1.2/vodstok vodstok
$ vodstok
Usage: vodstok [options] [VDS URL]
Options:
-h, --help show this help message and exit
-l, --list-endpoints List saved endpoints
-a ENDPOINT, --add-endpoint=ENDPOINT
Add an existing endpoint
-r ENDPOINT, --del-endpoint=ENDPOINT
Remove an existing endpoint
-p ENDPOINT, --publish=ENDPOINT
Publish an existing endpoint
-w DIRECTORY, --write=DIRECTORY
Set destination directory
-s, --size Displays distributed storage capacity
-v, --version Show version
-u, --update Update endpoints
Première étape, ajouter le point de stockage web nouvellement créé, ainsi que d'autres déjà existant:
$ vodstok -a http://www.monserveur.com/vodstok/ $ vodstok -a http://virtualabs.fr/vodstok/ $ vodstok -a http://www.depotware-network.net/vodstock/ $ vodstok -a http://vodstok.zengeek.org/
Lors de chaque ajout de point de stockage (ou endpoint), vodstok vérifie que celui-ci est bien fonctionnel.
Seconde étape, publier votre point de stockage:
$ vodstok -p http://www.monserveur.com/vodstok/
Etape ultime, récupérer de nouveaux points de stockage grâce à la fonctionnalité de mise-à-jour intégrée:
$ vodstok -u
Comment échanger des fichiers grâce à Vodstok
Pour échanger des fichiers, vous n'avez pas obligatoirement besoin d'installer un point de partage (mais ça serait quand même 'achement bien, pour la bonne santé et l'espace de stockage du système). Il est tout de même conseillé d'ajouter au moins un point de stockage existant, et de mettre à jour la liste des points de stockage:
$ vodstok -a http://virtualabs.fr/vodstok/ $ vodstok -u
Une fois cela fait, il est alors très facile d'uploader un fichier sur le réseau vodstok:
$ vodstok britney-spears-chante-sous-la-douche.mp3 [+] Uploading ... 100.00% [i] File successfully uploaded. Share this link: vds://054B22B4D64746A94CCE72AB410516C7@www.depotware-network.net/vodstock/#5e23d232a24a332d3e64f02d9b9c9350
Et c'est identique pour le téléchargement:
$ vodstok vds://9AAFC5E52E29E833D969399E9EEF3D0F@virtualabs.fr/vodstok/#6edc5a91f3c29190229822f0119f9745 [+] Retrieving file summary [+] Starting file download [+] Downloading ... 100.00% [i] File saved as britney-spears-chante-sous-la-douche.mp3
Source et contribution
J'ai mis le projet sur [GITHUB], et monté un repository Vodstok sur mon hébergement. N'hésitez pas à tester, jeter un oeil, et si vous pensez pouvoir apporter votre brique, à modifier le code et proposer vos améliorations.
Conclusion
Le projet vodstok est actuellement viable, mais toujours en phase beta. Je suis en train d'essayer d'améliorer le système et de faire en sorte que celui-ci soit fiable et efficace. Essayez-le, installez des points de stockage, et faites moi vos retours d'expérience par email (virtualabs -at- gmail -dot- com) ou directement via twitter !
`GITHUB] [https://github.com/virtualabs/Vodstok <https://github.com/virtualabs/Vodstok>`_ `HOMEPAGE] [http://virtualabs.fr/vodstok/ <http://virtualabs.fr/vodstok/>`_
Grand utilisateur de tickets de métro (émis par le STIF/RATP), je me suis posé la question un jour (oui, j'ai de ces idées pourries des fois) de savoir comment sont codées les informations sur ce support magnétique. L'idée m'a semblé intéressante à creuser, mais nécessitait un brin de bidouille électronique. L'occasion de tester mon nouveau fer à souder reçu à Noël, et de découvrir l'univers des bandes magnétiques et plus particulièrement celui des tickets de la RATP.
Les bandes magnétiques
J'ai fureté pendant des heures sur Internet, à la recherche de documentation sur la lecture des bandes magnétiques, ainsi que sur les normes de codage employées. J'ai déniché quelques papiers intéressants:
Ces trois ressources sont très intéressantes, le white paper du CCC étant celle qui m'a apporté le plus. J'en ai discuté au boulot avec Tixlegeek, et il m'a lui aussi soumis l'idée d'utiliser la carte son de mon ordinateur portable pour échantillonner les données stockées sur le ticket (à l'instar de ce qui est documenté dans le papier de Joseph Battaglia). Il s'agit là d'un hack archi-simple, vu qu'il suffit de trouver une tête de lecture d'un vieux lecteur cassette et de la souder sur un jack 3.5 puis de le brancher dans la prise micro. La carte son fera le reste.
En ce qui concerne le codage des données sur bande, celui-ci peut se faire de deux manières:
La première méthode est celle employée pour stocker des données audio, mais est peu robuste. La seconde permet de stocker de manière plus robuste, et indépendamment de la vitesse de lecture ou d'écriture, des données binaires. C'est celle qui est préférée dans la grande majorité des systèmes de stockage numériques sur bande.
Le codage de fréquence le plus classique a un joli surnom: F2F. Ou plus généralement connu sous le nom de Aiken Biphase. L'idée est de coder le 0 avec un signal de fréquence F, et le 1 avec un signal de fréquence 2F. L'avantage de ce codage, c'est que peu importe la vitesse de lecture (qui est variable selon les périphériques), le décodage reste fiable. Bon par contre ça complique un brin la phase de décodage. Patrick Gueulle explique très bien cela dans son livre "Carte magnétique et PC".
Traitement du signal
Le papier du CCC [2] présente deux programmes (nommés DAB et DMBS) visant à respectivement décoder le codage Aiken Biphase (F2F) et à extraire les données selon les normes ISO781X. Ces mêmes programmes ont été utilisés par K1wy dans [1]. Le souci c'est que ceux-ci ont été principalement conçus pour les cartes magnétiques rigides à trois pistes, pas forcément pour les tickets de métro. De plus, selon les réglages audio de l'ordinateur servant à la capture (Mic-Boost notamment). Il me fallait une solution plus malléable, car ces deux programmes sont écrits en C et pas forcément bien documentés.
J'ai trouvé plusieurs modules python permettant de réaliser la capture d'un signal sur l'entrée micro et le traitement de celui-ci:
A l'aide de ces modules, j'ai réalisé quelques scripts de capture de signal et de sauvegarde des données sous format PCM et WAV. l'avantage de pouvoir sauvegarder les captures audio est double: on peut facilement refaire des analyses et surtout on peut les lire avec des outils comme Audacity. J'ai donc réalisé un script de capture du signal, et un script de traitement visant à traiter le signal afin de mettre en évidence le codage fréquentiel.
J'ai réalisé les tests de capture à l'aide d'un micro, et utilisé pylab pour afficher l'allure du signal. Pour le coup, c'était assez efficace:
Il m'a fallut ensuite réaliser mon lecteur de bande magnétique à la manière du CCC, à l'aide d'un vieux lecteur de cassette audio, d'un fer à souder, d'un jack 3.5 et d'un cable IDE =). J'ai du sacrifier mon super lecteur CD/K7 de mes 18 ans (de toute façon je n'ai plus de K7), mais c'est pas grave.
Fabrication du lecteur de bande magnétique maison
Premièrement, j'ai démonté le lecteur pour pouvoir accéder à la partie électro-mécanique au niveau du lecteur de K7:
Une fois les têtes de lecture et d'écriture démontées, j'ai pu souder la tête de lecture (stéréo, mais on s'en moque) sur le jack 3,5mm. Bon la méthode de connection étant pas trop documentée, j'ai un peu galéré mais j'y suis arrivé. Pour information, si vous essayez de refaire le montage chez vous et que vous employez un jack 3,5mm stéréo, faites très attention au contact central: il délivre du 5V (cette tension est utile pour les microphones de type Electret). Pour ma part j'ai pas gazé car j'aurais du relier la sortie de la seconde bobine de lecture à la première, pour cumuler les tensions de sortie, alors que dans mon montage je l'ai mise à la masse (donc je n'utilise qu'une seule des deux bobines de lecture).
J'ai utilisé trois fils couplés d'un cable IDE, de manière à avoir de quoi manipuler la tête de lecture et l'éloigner de l'ordinateur portable (pour limiter les interférences). Bon au final ça n'a pas tellement aidé, le cable IDE faisant une merveilleuse antenne. Une meilleure solution serait d'utiliser un cable blindé à trois brins dont le blindage est mis à la masse. Une fois celui-ci connecté à mon ordinateur portable, j'ai pu balayer un ticket de métro et capturer avec mon précédent script le signal issu de la tête de lecture (signal brut).
Décodage du signal et analyse
A l'aide de ce lecteur de bande magnétique fait maison et de mes quelques scripts, j'ai codé un second script permettant de mettre en évidence les données codées sur la bande magnétique, et ça n'a pas été une paire de manche. J'ai réalisé trois décodages pour la forme:
Et voilà les données codées (Aiken Biphase, ou F2F) extraites des captures de signaux:
Le premier est issu d'un ticket neuf, et les deux suivants de deux tickets a priori validés à Saint Lazare (Paris). Il est flagrant de voir qu'un motif général est présent, motif que l'on retrouve en partie dans le ticket de métro neuf. Chose intrigante, le ticket de métro neuf possède moins d'information que les tickets compostés. J'en déduis donc que des données sont ajoutées lors de la validation, et qu'un ticket de métro neuf est codé d'une manière particulière.Si vous regardez bien, il y a de légères variations de période, mais cela est dû au fait que je scanne manuellement, et ma vitesse de balayage n'est pas constante.
J'ai donc entrepris de décoder les premiers bits significatifs communs aux trois tickets, et voici ce qui en ressort:
Ticket neuf: 11111111 00100 11111 11111 11111 0 Ticket #1: 11111111 00100 11111 00110 01001 0 Ticket #2: 11111111 00100 11111 00110 01110 1
Les espaces sont de mon fait ;). J'ai pu déduire le rôle de chacun (ou du moins tenter de deviner) à partir des informations décodées:
Si on regarde de plus près le codage, on peut voir que sur le premier ticket validé les troisième et quatrième séquences de 5 bits correspondent aux valeurs 69 et 6E. Ou plus précisemment aux valeurs 6 et 9, et 6 et 14. Cela ressemble bien aux codes de stations connus de Paris, tels que décrit sur Wikipédia. Cela signifie qu'un de mes tickets a été validé à Saint-Lazare même (code station 0609), l'autre à la station ayant pour code 0614 (inconnu sur Wikipédia oO). Le dernier bit que j'ai repéré semble être un bit de parité.
Le plus drôle dans l'histoire, c'est de voir que le ticket vierge n'a aucune valeur paramétrée, si ce n'est le type de ticket (première séquence de 5 bits). Donc facile à cloner.
Conclusion
La sécurité des tickets de métro ... heuu.... quelle sécurité ? Il est a priori aisé de dupliquer des tickets neufs (aucune limite dans le temps ni dans l'espace}, la seule limitation est technique (il faut un matériel particulier). De même, je n'ai pas détaillé ici le décodage complet mais la suite des bits stockés représentent très certainement la date de validation et l'heure de validation, mais je n'ai pas vraiment cherché à le décoder.
Heureusement qu'il y a le passe Navigo pour tous nous sauver (spéciale dédicace à Nono2357 ;).