Injections SQL (SQLi) : principes, impacts, exploitations et bonnes pratiques de sécurité

La plupart des applications web utilisent une ou plusieurs base(s) de données pour stocker et traiter les informations en temps réel.

En effet, lorsqu’un utilisateur envoie des requêtes, l’application web interroge la base de données afin de construire la réponse. Cependant, lorsque les informations fournies par l’utilisateur sont utilisées pour forger la requête à la base de données, un attaquant peut altérer cette dernière en l’utilisant à d’autres fins que celles prévues par le développeur d’origine. Ainsi, cela permet à un attaquant d’interroger la base de données via une injection SQL, abrégé en SQLi.

L’injection SQL fait référence aux attaques contre les bases de données relationnelles telles que MySQL, Oracle Database ou Microsoft SQL Server. En revanche, les injections contre les bases de données non relationnelles, telles que MongoDB ou CouchDB, sont des injections NoSQL.

Principes, impacts, exploitations, nous vous présentons dans cet article une vue d’ensemble des injections SQL, ainsi que les bonnes pratiques sécurité et mesures à implémenter pour contrer les risques d’attaque.

En quoi consiste une injection SQL (SQLi) ?

Il existe de nombreux types de vulnérabilités par injection, comme les failles XSS, l’injection d’entête HTTP, l’injection de code ainsi que l’injection de commande. Cependant, la plus connue, l’une des plus redoutables et la favorite des attaquants est certainement l’injection SQL.

Une injection SQL se produit lorsqu’un utilisateur malveillant communique une entrée qui modifie la requête SQL envoyée par l’application web à la base de données. Cela lui permet alors d’exécuter d’autres requêtes SQL non souhaitées directement sur la base de données.

Pour ce faire, l’attaquant doit injecter du code en dehors des limites de l’entrée utilisateur attendue, afin qu’il ne soit pas exécuté comme une entrée standard. Dans le cas le plus simple, il suffit d’injecter un guillemet simple ou double pour échapper aux limites de la saisie utilisateur et ainsi insérer des données directement dans la requête SQL.

En effet, si possibilités d’injection il y a, l’attaquant va chercher un moyen d’exécuter une requête SQL différente. Dans la plupart des cas, il va utiliser du code SQL pour créer une requête qui exécute à la fois la requête SQL prévue et la nouvelle requête SQL.

Cas d’utilisation et impacts d’une injection SQL

Une injection SQL peut avoir un impact énorme, surtout si les privilèges sur le serveur et sur la base de données sont trop permissifs.

Tout d’abord, un attaquant peut récupérer des informations sensibles, comme les identifiants et mots de passe des utilisateurs ou les informations relatives aux cartes bancaires. En effet, les injections SQL sont à l’origine de nombreuses compromissions de mots de passe et de données de sites et d’applications web.

Un autre cas d’utilisation de l’injection SQL consiste à détourner la logique prévue de l’application web. L’exemple le plus courant est le contournement d’une page d’authentification. Les attaquants peuvent également être en mesure de lire et d’écrire des fichiers directement sur le serveur, ce qui peut conduire à placer des backdoors (portes dérobées) sur le serveur, puis à prendre le contrôle de l’application.

Comment détourner la logique d’une application via une attaque par injection SQL ? 

Avant de commencer à exécuter des requêtes SQL entières, nous allons d’abord étudier comment détourner la logique de la requête originale.

Recherche d’un paramètre vulnérable aux SQLi

Avant de parvenir à nos fins, à savoir détourner la logique de l’application web et contourner l’authentification, nous devons dans un premier temps tester le formulaire de connexion, pour savoir s’il est vulnérable à l’injection SQL.

Pour ce faire, nous pouvons ajouter l’une des payload ci-dessous après notre nom d’utilisateur et voir si cela provoque des erreurs ou modifie le comportement de la page :

PayloadURL Encoded
%27
« %22
#%23
;%3B
)%29

Lors de l’ajout d’un simple guillemet, une erreur SQL est affichée.

Affichage d'une erreur SQL

La requête SQL envoyée à la base de données est la suivante :

Envoi requête SQL à une bdd

Le guillemet que nous avons saisi a donné lieu à un nombre impair de guillemets, ce qui a provoqué une erreur de syntaxe. Une option serait de commenter et d’écrire le reste de la requête dans le cadre de notre injection pour forger une requête fonctionnelle. Une autre option consiste à utiliser un nombre pair de guillemets dans notre requête injectée, de sorte que la requête finale fonctionne toujours.

Contournement d’authentification via une attaque par injection SQL

Sur cette page d’authentification, nous pouvons nous connecter avec les informations d’identification de l’administrateur :

Identifiant : admin 
Mot de passe : Vaada7aPa55w0rd!

La page affiche la requête SQL en cours d’exécution afin de mieux comprendre comment détourner la logique de la requête.

La page prend en compte les informations d’identification, puis utilise l’opérateur AND pour sélectionner les enregistrements correspondants au nom d’utilisateur et au mot de passe renseignés. Si la base de données MySQL renvoie les enregistrements correspondants, les informations d’identification sont valides, et le code PHP évalue la condition de tentative de connexion comme vraie. Si la condition est « True », l’enregistrement de l’administrateur est renvoyé, et notre connexion est validée.

Au contraire, lorsque de mauvaises informations de connexion sont renseignées, la connexion échoue et la base de données renvoie « False ».

Attaque par injection SQL avec l’opérateur OR

Pour contourner l’authentification, il faudrait que la requête renvoie « True », quels que soient le nom d’utilisateur et le mot de passe saisis. Pour ce faire, nous pouvons abuser de l’opérateur OR dans notre injection SQL.

La documentation MySQL indique que l’opérateur AND est évalué avant l’opérateur OR. Cela signifie que s’il y a au moins une condition « True » dans la requête avec un opérateur OR, la requête sera évaluée comme « True » puisque l’opérateur OR renvoie « True » si l’un de ses opérandes est vrai.

Un exemple de condition qui renvoie toujours TRUE est 1=1. Toutefois, pour que la requête SQL continue de fonctionner et que le nombre de guillemets soit pair, au lieu d’utiliser (‘1’=’1’), nous supprimerons le dernier guillemet et utiliserons (‘1’=’1), de sorte que le guillemet unique restant de la requête originale sera à sa place.

L’opérateur AND sera évalué en premier, et il renverra « False ». Ensuite, l’opérateur OR sera évalué, et si l’une des déclarations est vraie, il renverra « True ». Puisque 1=1 renvoie toujours « True », cette requête renverra vrai et nous donnera l’accès.

Note : La payload que nous avons utilisé ci-dessus est l’une des nombreuses payloads de contournement d’authentification que nous pouvons utiliser pour contourner la logique d’authentification.

Si le nom d’utilisateur n’est pas valide, la connexion va échouer parce qu’il n’existe pas dans la table et a donné lieu à une fausse requête globale.

Contournement de l’authentification avec des commentaires

Comme tout autre langage, SQL permet également l’utilisation de commentaires. Les commentaires sont utilisés pour documenter les requêtes ou ignorer une certaine partie de la requête. Nous pouvons utiliser deux types de commentaires avec MySQL : — et #.

Comme nous pouvons le voir, le reste de la requête est maintenant ignoré et le mot de passe n’est plus vérifié. De cette façon, nous pouvons nous assurer que la requête ne présente aucun problème de syntaxe.

Focus sur les attaques par injection SQL avec UNION (Union Based SQLi)

Un autre type d’injection SQL consiste à injecter des requêtes SQL entières exécutées en même temps que la requête originale.

La clause UNION est utilisée pour combiner les résultats de plusieurs instructions SELECT. Cela signifie que grâce à une injection UNION, nous serons en mesure de sélectionner et d’extraire des données de l’ensemble de la base de données.

UNION a combiné la sortie des deux instructions SELECT en une seule, ainsi les entrées des tables ont été combinées en une seule sortie.

Une instruction UNION ne peut fonctionner que sur des instructions SELECT comportant un nombre égal de colonnes. L’UNION de deux requêtes qui ont des résultats avec un nombre de colonnes différentes renverra une erreur.

S’il y a plus de colonnes dans la table de la requête originale, il faut ajouter d’autres chiffres afin de créer les colonnes restantes requises.

Comme nous pouvons le voir, le résultat souhaité de la requête se trouve dans la première colonne de la deuxième ligne, tandis que les chiffres remplissent les autres colonnes.

Identification du nombre de colonnes

Afin d’exploiter les requêtes basées sur la clause UNION, il faut trouver le nombre de colonnes sélectionnées par le serveur. Il existe deux méthodes pour détecter ce nombre :

  • En utilisant ORDER BY
  • En utilisant UNION

Utilisation de ORDER BY

La première façon de détecter le nombre de colonnes est la clause ORDER BY. La requête injectée va trier les résultats par le nombre de colonne que nous avons spécifiée jusqu’à ce que nous obtenions une erreur indiquant que la colonne spécifiée n’existe pas. La dernière colonne par laquelle nous avons réussi à trier nous donne le nombre total de colonnes.

Utilisation de UNION

L’autre méthode consiste à utiliser la clause UNION avec un nombre différent de colonnes jusqu’à ce que nous obtenions les résultats avec succès. Contrairement à la méthode précédente, celle-ci donne toujours une erreur jusqu’à ce que nous obtenions le bon nombre de colonnes.

Localisation de l’injection

Alors qu’une requête peut renvoyer plusieurs colonnes, l’application web peut n’en afficher que certaines. Ainsi, si nous injectons notre requête dans une colonne qui n’est pas affichée sur la page, nous n’obtiendrons pas son résultat. C’est pourquoi nous devons déterminer quelles colonnes sont présentes sur la page, afin de déterminer où placer notre injection.

Énumération de la base de données suite à un SQLi

Avant d’énumérer la base de données, nous devons identifier le type de Système de Gestion de Base de Données (SGBD) afin de savoir quelles requêtes utiliser.

Si le serveur web que nous voyons dans les réponses HTTP est Apache ou Nginx, il est probable que le serveur web soit sous Linux, et donc que le SGBD soit MySQL. Il en va de même pour le SGBD Microsoft si le serveur web est IIS, il s’agit donc probablement de MSSQL. Il existe donc différentes requêtes que nous pouvons tester pour déterminer le type de base de données.

Maintenant, pour extraire des données des tables à l’aide de UNION SELECT, nous devons former correctement nos requêtes SELECT. Pour ce faire, nous devons disposer de :

  • La liste des bases de données
  • La liste des tables de chaque base de données
  • La liste des colonnes de chaque table

Avec les informations ci-dessus, nous pourrons formuler notre instruction SELECT pour extraire toutes les données.

La base de données INFORMATION_SCHEMA contient des métadonnées sur les bases de données et les tables présentes sur le serveur. Cette base de données joue un rôle crucial dans l’exploitation des vulnérabilités par injection SQL.

Schema

Pour trouver quelles bases de données sont disponibles sur le SGBD, nous pouvons utiliser INFORMATION_SCHEMA.SCHEMATA, qui contient des informations sur toutes les bases de données du serveur.

Tables

Pour trouver toutes les tables d’une base de données, nous pouvons utiliser INFORMATION_SCHEMA.TABLES. Cette opération peut être effectuée de la même manière que celle qui a permis de trouver les noms des bases de données.

Colonnes

Pour trouver les noms des colonnes de la table, nous pouvons utiliser la table COLUMNS de la base de données INFORMATION_SCHEMA. Elle contient des informations sur toutes les colonnes présentes dans toutes les bases de données.

Données

Maintenant que toutes les informations sont réunies, nous pouvons former notre requête UNION pour extraire les données des de la base de données.

Lecture de fichiers suite à une SQLi

Une injection SQL peut également être utilisée pour effectuer de nombreuses autres opérations, telles que la lecture et l’écriture de fichiers sur le serveur et même l’exécution de code à distance sur le serveur.

La lecture de données est beaucoup plus courante que l’écriture de données, qui est strictement réservée aux utilisateurs privilégiés dans les SGBD modernes, car elle peut conduire à l’exploitation du système. Dans MySQL, l’utilisateur de la base de données doit disposer du privilège FILE pour charger le contenu d’un fichier dans une table.

Plusieurs requêtes SQL permettent de déterminer quel utilisateur exécute les requêtes.

On peut désormais lister les privilèges des utilisateurs. Nous constatons que le privilège FILE est listé pour notre utilisateur, ce qui nous permet de lire des fichiers et même potentiellement d’en écrire.

Grâce à ce privilège FILE, un utilisateur est capable de lire les fichiers du serveur.

Écriture dans des fichiers suite à une SQLi

L’écriture de fichiers sur le serveur peut être utilisé pour écrire un webshell sur le serveur distant, ce qui permettra d’exécuter du code et de prendre le contrôle du serveur.

De la même manière que pour la lecture de fichiers, si l’utilisateur possède les privilèges suivants, il sera capable d’écrire sur le serveur :

  • Privilège FILE activé
  • Variable globale MySQL secure_file_priv n’étant pas activée.
  • Un accès en écriture à l’emplacement où il veut écrire sur le serveur.

L’instruction SELECT INTO OUTFILE peut être utilisée pour écrire des données dans des fichiers à partir de requêtes de sélection. Elle est généralement utilisée pour exporter des données depuis des tables.

Un attaquant peut ainsi uploader un webshell et ainsi accéder au serveur.

Comment contrer les attaques par injection SQL ?

Dans cette partie, nous allons apprendre à éviter ces types de vulnérabilités dans notre code et à les corriger lorsqu’elles sont découvertes.

Assurer une bonne gestion des privilèges utilisateurs

Les logiciels SGBD permette de créer des utilisateurs avec des permissions très fines. Nous devons nous assurer que l’utilisateur qui interroge la base de données ne dispose que des permissions minimales.

Utiliser des requêtes préparées

L’utilisation de requêtes paramétrées est un autre moyen de s’assurer que l’entrée est sécurisée. Les requêtes paramétrées contiennent des espaces réservés aux données d’entrée, qui sont ensuite échappées et transmises par les pilotes. Au lieu de transmettre directement les données dans la requête SQL, nous utilisons des espaces réservés et les remplissons ensuite avec des fonctions PHP.

La requête est modifiée pour contenir deux espaces réservés, marqués par des « ? » où le nom d’utilisateur et le mot de passe seront placés. Nous lions ensuite le nom d’utilisateur et le mot de passe à la requête en utilisant la fonction mysqli_stmt_bind_param(). Cette fonction permet d’échapper aux guillemets et de placer les valeurs dans la requête.

Autres articles sur des attaques et vulnérabilités courantes des applications :

Auteurs : Alexis MARTIN – Pentester @Vaadata & Amin TRAORÉ – CMO @Vaadata