Rédacteur : Trance
Date de création : 02/07/2006
Section : Sécurité > Failles Web
Imprimer cet article : en noir et blanc ou en couleurs
Les injections SQL font partie des failles Web redoutables, puisqu'elles s'exploitent côté serveur. Elles touchent les sites qui interagissent de manière non sécurisée avec une base de données, permettant ainsi à un attaquant de détourner les requêtes comme il le souhaite. Nous verrons ici les bases de ce type de failles, et quelques techniques d'injections plus poussées, mais sans rentrer dans le détail des "Blind SQL Injections".
Le principe de base qui explique la présence d'une faille d'injection SQL est très simple. Observons ce code pour comprendre :
Bien qu'un peu long, ce code montre un exemple complet de page. Quel est son rôle ? C'est un moteur de recherche dans des articles, tout simplement. La vulnérabilité se situe dans le code en rouge. En effet, la requête effectuée est la suivante :
En vert se trouve le contenu encadré par des guillements simples ; c'est ce que va rechercher le script dans la table des
articles, à l'aide de l'opérateur LIKE.
En effet, LIKE utilisé avec le caractère '%' permet de chercher du
contenu qui "ressemble" à ce qui est tapé par l'utilisateur, car le %
est interprété par LIKE comme "n'importe quelle chaîne de caractères".
C'est un peu comme une expression régulière ; si vous recherchez "%un%
", vous pourrez obtenir n'importe quelle chaîne de caractère contenant "un", comme "une", "lune", et "brun".
Mais cet opérateur ne constitue pas une faille. La vulnérabilité est due au fait que l'entrée $_POST["rech"]
n'est pas filtrée. Car nous pouvons imaginer qu'un utilisateur rentre cette chaîne : "n'importe quoi
". A ce moment là, la requête devient :
Et l'on remarque très vite le problème qui va se poser : une erreur de syntaxe due au guillement, que mysql intermprète comme la fin de la chaîne de caractères. La requête ne sera pas exécutée, et le visiteur obtiendra :
Maintenant, imaginons un visiteur plus expérimenté, et qui connaît ce type de faille. Il teste une première fois le script en rentrant "n'importe quoi", le script plante et lui affiche la reqête fautive. Ensuite, voici ce qu'il pourraît rentrer dans le formulaire :
La requête devient donc :
Ici, le script ne plantera pas ; il n'y a aucune erreur de syntaxe.
Le script ressortira la liste de tous les articles, puisque la
condition 'a%' = 'a%'
est toujours réalisée.
Ceci était un exemple de détournement de requête, mais il n'est pas très utile. Cependant, il est tout à fait possible de détourner le script afin d'obtenir des renseignements utiles sur le serveur. C'est ce que nous allons bientôt voir.
Laissons cet exemple de côté et penchons-nous plutôt sur un deuxième :
Ceci est un exemple très basique de formulaire de zone admin. Et non sécurisé... Mis à part qu'il soit vulnérable à une XSS, il l'est également pour les injections SQL. En effet, si nous rentrons ceci :
La requête en rouge devient
Ainsi, on peut se loguer sans connaître l'utilsiateur et le mot de passe, puisque la consition 'a'='a' est toujours vérifiée.
On se rend donc compte que les injections SQL peuvent devenir très dangereuses si l'administrateur a une politique de sécurité négligente, ou s'il n'en a pas du tout...
Si vous êtes webmaster, cela vous intéressera sûrement de savoir comment corriger ce type de failles. En réalité, c'est très simple, encore faut-il prendre le temps de se documenter à ce sujet...
Le premier réflexe que l'on peut avoir est de se dire "les guillements sont à proscrire, ce sont eux les causes de ce type de failles", et donc de coder un script qui enlève tous les guillements ou qui les échappe en rajouttant un antishash devant. On peut donc utiliser la fonction addslashes() ou bien utiliser les options magic_quotes_gpc et magic_quotes_runtime qui le font en parmanence pour nous. Ainsi, ce code est sécurisé :
La fonction que nous avons codé, secure(), ajouttera des antislashes si nécessaire devant tous les ', ce qui les rendra interprétés par mysql non en tant que fin de chaîne mais comme des simples caractères. Elle utilise la fonction mysql_real_escape_string(), conçue spécialement pour éviter ce type de failles.
Mais le problème de ce genre de solution apparaît très clairement lorsque nous voulons effectuer des requêtes faisant intervenir des nombres :
En effet, ici, il sera toujours possible de faire des injections SQL
car il n'y a pas de guillemets encadrant le nombre (ici,
$_POST["idarticle"]). On peut très bien taper "0 OR 1=1
" afin de lister tous les articles. Même si c'est inutile, cela permet quand même de prouver l'existence de la faille.
On en conclut que même si une fonction de sécurisation des guillements est présente, qu'elle soit automatique ou non (magic_quotes), il y a toujours un risque. Pour l'éliminer définitivement, il faut donc faire :
On a rajouté des guillements simples encadrant le nombre, ce qui ne gène en rien Mysql. Et cela rend le script sécurisé...
Pour résumer, la solution pour se protéger des injections SQL est simple : il ne faut jamais faire confiance à l'utilisateur. Ne jamais le sous-estimer et penser qu'il n'est pas informé. Il faut alors filtrer tout ce que peut rentrer l'utilisateur avant de faire quoi que ce soit.
De plus, vous ne devez jamais laisser paraître les erreurs SQL sur votre site, car une personne malveillante peut faire planter le script volontairement afin de recueuillir des informations sur le nom des champs et des tables du serveur. C'est pour cela qu'il vaut mieux retourner un code d'erreur si une fonction échoue, plutôt que d'affiacher la requête. D'ailleurs, ici, cet affichage provoque une vulnérabilité de type XSS, car l'utilisateur malin qui injectera du javascript dans le champ fera d'une part échouer l'interaction avec la base, et d'autre part afficher son code...
Maintenant, nous allons introduire l'exploitation de ces failles de manière avancée à travers quelques exemples. Ne perdez pas de vue le fait que ces exemples ne sont pas des cas réels ; ici, nous voulons juste montrer l'étendue de ces failles. Dans un autre articles, les "Blind Injections SQL", nous verrons concrètement comment l'on se débrouille en réalité. Je vous conseille donc de vous repporter à cet article pour obtenir des cas plus concrets.
Tout d'abord, reprenons le premier exemple, le moteur de recherche. Ce script très mal conçu affiche la requête lorsqu'il plante, donc nous allons tirer parti de cet affichage pour noter le nom des tables et des champs. Pour le deuxième exemple, nous pouvons aussi faire planter le script en mettant un guillement, ce qui nous révèle le nom de la table "membres", et des deux champs "login" et "pass", assez explicites. A présent, essayons de récupérer le login et le pass de l'admin. Nous voulons afficher ces informations. Il nous faut donc trouver un script sur le site vulnérable qui en affiche. Par exemple, le moteur de recherche affiche les articles. A l'attaque ! Voici ce que nous pouvons rentrer dans le champ :
Notez que UNION n'est disponible que depuis MySQL 4. La requête devient :
Le # représente pour MySQL un commentaire ! Remarquez que l'on peut aussi utiliser '/*'. Par conséquent, tout ce qui est après sera ignoré. La requête sélectionne donc tous les articles et ajoute (rôle du mot clé UNION) tous ces résultats à la liste des administrateurs et leur mot de passe. Notez l'astuce consistant à renomer à l'aide de AS les champs afin de réussir l'UNION. De plus, si vous croisez une table A de n champs avec une table B, B devra forcément avoir elle aussi n champs, qui devront avoir le même type. C'est pour cela que l'on sélectionne trois champs dans chaque partie de la requête. Cela peut donc produire cet affichage :
On a donc les mots de passe des deux admins, et le comble c'est qu'ils sont codés en clair !
Imaginons maintenant que nous disposons du nom d'une table et d'un champ dans cette table. Nous voulons obtenir le nom d'un champ et d'une autre table où l'on pourrait trouver des informations utiles.
La méthode à utiliser est de la divination pure et dure ;-). On peut essayer ainsi les tables "articles", "membres", "admin", "users", etc. et les champs corespondants "login", "password", "pass", etc. Mais comment tester justement ces champs et ces noms de tables ? Nous allons utiliser par exemple cette requête :
Deviner le nom d'une table
En blanc, la requête initiale, en vert, ce que l'on rentre et en bleu le commentaire. On remarque que l'on n'a fait que
dupliquer la requête en ajoutant juste ",membres
" dans le nom des tables à interroger. La requête va donc
planter si jamais "membres" n'est pas le nom d'une table existante, et afficher des articles dans le cas contraire.
Il faut donc remplacer "membres" par le nom de la table à tester.
Encore une fois, je le rappelle, tous ces exemples ne sont pas ceux que l'on utiliserait dans la réalité puisqu'ils necessitent de connaître pas mal de renseignements sur la base. Repportez-vous aux Blind SQL Injections.
Deviner le nom d'un champ dans une table connue
Nous connaissons le nom de la table (membres), nous voulons savoir le nom des champ. Nous testons :
"champatester" est le champ dont nous voulons vérifier la présence. Si la requête réussit, il existe, sinon, il n'existe pas.
Deviner un mot de passe
Dans certains cas il arrive que le contenu d'un champ soit accesible mais non affiché. C'est le cas par exemple de notre formulaire de zone admin : on ne peut pas directement afficher le mot de passe. Il faut donc procéder par essais successifs :
A croiser bien sûr avec une injection SQL. Si la session d'admin s'ouvre, c'est que son mot de passe commence par un 'a'. Sinon, on essaye avec 'b', puis ainsi de suite. On remonte les caractères jusqu'à trouver le mot de passe complet.
Si l'on veut connaître le nombre de caractères du mot de passe, on peut utiliser l'opérateur LENGTH() comme ceci :
Les méthodes que nous venons de voir se font par bruteforcing, puisqu'elles ne nous permettent pas d'obtenir les informations de manière directe, mais simplement de savoir si l'on a bon ou faux. L'opérateur AS est très pratiques dans ces situations puisqu'il permet de renomer un champ temporairement pour la requête, donc de croiser deux champs différents.
SHOW et GRANT : obtenir tout ce que l'on veut (?)
Les deux opérateurs ultimes dont nous aurions besoin sont GRANT et SHOW. Ils permettent de lister la structure complète de la base, des tables, lister les utilisateurs, leurs privilèges et leur mot de passe. Le seul problème majeur est qu'il est impossible de les utiliser en les croisant avec SELECT. Impossible donc d'y avoir accès dans les exemples que nous venons de voir.
Les petits malins auront envie d'utiliser le point virgule afin d'effectuer deux requêtes l'une après l'autre, mais un autre problème nous en empêche : mysql_query(). Cette fonction interagit entre PHP et MySQL mais ne peut permettre que d'effectuer UNE SEULE requête à la fois. Le point virgule est donc à bannir. Et GRANT/SHOW par la même occasion... sauf si c'est une autre fonction qui est utilsée, et qui permet ce genre de choses !
Ainsi, vous sere peut-être déçu mais il est impossible d'utiliser un opérateur tel que UPDATE, DROP, ou INSERT si la requête commence par SELECT. A moins, comme nous l'avons dit, que la fonction utilisée le permette.
Obtenir la version du serveur
Nous pouvons utiliser les variables spécifiques à chaque serveur SQL. Pour MYSQL, la variable @@version
contient le nom de la version du serveur. Ainsi, SELECT @@version
affiche cette version. Il faut bien entendu exploiter une injection
afin de pouvoir utiliser cette commande. Pour les autres serveurs,
c'est une autre variable. Il suffit de chercher un peu dans les
documentations pour les trouver.
Créer un fichier
La directive INTO OUTFILE [nom_fichier]
croisée avec SELECT permet de créer un fichier contenant les
informations de SELECT demandées. On peut donc imaginer un scénario où
vous exploitez tout d'abord une injection SQL se situant dans une
requête INSERT ce qui vous permet d'écrire du code dans la base, puis
vous exploitez une deuxième injection dans SELECT afin de créer un
fichier contenant le code que vous avez écrit dans la base. Si c'était
du code PHP, une backdoor par exemple, ou un simple script d'une ligne
contenant une faille Include, vous obtiendrez un accès supplémentaire
sur le serveur.
Cependant, il faut déja obtenir l'url vers laquelle écrire le fichier, car si vous écrivez à la racine du serveur, cela peut d'une part sembler louche, d'autre part ne pas être accessible sur le serveur... En effet, si votre fichier écrit se trouve avant le répertoire "www" d'Apache, c'est fichu...
Utiliser un fichier pour effectuer des opérations
Il est possible d'utiliser LOAD DATA INFILE [nom_fichier]
pour lire un fichier et effectuer les
opérations SQL décries à l'intérieur. C'est le complément de INTO
OUTFILE. Le seul problème, c'est que LOAD n'est pas compatible avec
SELECT, donc inutilisable dans la grande majorité des cas.
Vous l'aurez compris, les injections SQL dépassent complétement le cas d'école avec ' or ''='
.
Il y en a des tonnes, plus ou moins adaptées à ce que l'on veut faire.
Les seuls limites sont celles du langage SQL, qui nous empêche parfois
de croiser des requêtes pourtant bien pratiques... Dans un second article
sur les injections SQL dites "blind", ou "à l'aveuglette", nous
exploiterons à fond le langage SQL afin de faire révéler aux scripts
vulnérables des informations cruciales.
Le but de cet article n'était bien entendu pas d'inciter les gens à exploiter des failles sur des sites existants, mais surtout de comprendre le fonctionnement de ce type de failles, afin de mieux savoir s'en protéger, par exemple.
Cet article a été écrit en grande partie à l'aide de mes connaissances, mais voici quelques liens qui vous permettront d'approfondir le sujet :