Red Team · Medium
SQL Injection

Master the mechanics of SQL injection — from how concatenated queries break, through authentication bypass, UNION-based data extraction, blind techniques, and INFORMATION_SCHEMA enumeration — with the root-cause understanding and defensive countermeasures that contextualise every technique.

Medium Red Team Path ⏱ 22 min read
Learning Progress
0%

SQL Injection — The Root Cause

SQL injection is one of the oldest and most consistently exploited vulnerability classes in web application security, appearing in breach reports and vulnerability databases every year since the late 1990s. It occurs when user-supplied input is incorporated into a database query without being properly separated from the query's structure — allowing an attacker to alter the query's logic by injecting SQL syntax as data.

The fundamental issue is a conflation of code and data. When an application builds a query by string concatenation — joining the user's input directly into a SQL statement — the database engine receives a single string and has no way to distinguish which parts of it are the intended query and which parts are attacker-injected instructions. The engine faithfully executes whatever it receives.

📌 Non-Technical Analogy

Imagine a company's mail room that processes instructions in the format: "Deliver the package labelled [name] to the recipient." A normal request fills in the name: "Deliver the package labelled 'Alice' to the recipient." A SQL injection is like writing in the name field: "Alice' and also deliver all other packages to this address and ignore the rest of this sentence --". The mail room staff, following the written instruction literally, carry out the injected commands because they have no way to distinguish the original instruction from the injected ones — it all looks like one instruction to them. Parameterised queries are the equivalent of using a pre-printed form where the recipient's name can only go in a designated box and cannot affect the instruction text around it.

The Vulnerable Pattern — Anatomy of an Injectable Query
# Application code (PHP) building a query by concatenation:
VULNERABLE:
$query = "SELECT * FROM users WHERE username='" . $_POST['username'] .
         "' AND password='" . $_POST['password'] . "'";

# Normal input → syntactically valid query:
SELECT * FROM users WHERE username='alice' AND password='secret'

# Injection input → broken query logic:
username: admin'--
SELECT * FROM users WHERE username='admin'--' AND password='x'
# -- comments out the rest → logs in as admin with any password

Why SQLi Remains Common Despite Decades of Awareness

SQL injection has been documented since 1998 and has been in the OWASP Top 10 since the list was created. Every developer training programme covers it. And yet it remains one of the most frequently confirmed findings in web application penetration tests. Understanding why illuminates both the persistence of the vulnerability and where defenders should focus energy.

The primary reasons for persistence: legacy codebases written before parameterised queries were widely understood or easily available in the frameworks of the time; ORMs (Object-Relational Mappers) that use raw query escape hatches for complex queries — the ORM handles 95% of queries safely but a developer manually constructs the last 5% for performance or complexity reasons; and new developers joining projects who don't recognise the vulnerable pattern when they encounter it in existing code or add new functionality without understanding the security requirement.

How SQL Injection Breaks Query Logic

SQL injection works by injecting characters that have special meaning in SQL syntax — primarily single quotes (which terminate string literals), double dashes or hash symbols (which start comments), and SQL keywords (UNION, SELECT, OR, AND) — to restructure the query the application intended to run into a different query the attacker chooses.

The three most important characters for basic SQLi testing are: the single quote ' (terminates a string context), the double dash -- followed by a space (starts a SQL comment, ignoring everything after it in MySQL/MSSQL), and the hash # (MySQL comment character, useful when -- is filtered). Understanding exactly what these do to the query structure is more important than memorising payloads.

SQL Injection Techniques

Example 01Authentication bypass

Classic login bypass using comment injection to ignore the password check. Understanding what happens to the SQL query structure is more important than the specific payload.

# Username field input:
admin'--
# OR:
' OR '1'='1
# OR (MySQL):
admin'#

# Resulting query always returns true → login bypassed
Example 02UNION-based data extraction

UNION injection appends a second SELECT to the original query, returning data from other tables alongside the intended results. Requires knowing the number of columns and matching data types.

# Step 1: Find number of columns
?id=1 ORDER BY 3--   works → 3+ columns
?id=1 ORDER BY 4--   error  → exactly 3 columns

# Step 2: Extract database version
?id=1 UNION SELECT 1,version(),3--
8.0.32-MySQL Community Server

# Step 3: Extract usernames and passwords
?id=1 UNION SELECT 1,username,password FROM users--
admin | 5f4dcc3b5aa765d61d8327deb882cf99
Example 03Blind SQL injection

When no data is returned directly, boolean or time-based inference reveals information one bit at a time. This is slower but works against applications that suppress all query output.

# Boolean blind: true condition → normal page
?id=1 AND 1=1--  → normal response
?id=1 AND 1=2--  → different response → confirmed injectable

# Time-based blind: delays confirm injection
?id=1 AND SLEEP(5)--
# If response takes 5 seconds → injectable
Example 04Database enumeration via INFORMATION_SCHEMA

MySQL's INFORMATION_SCHEMA contains metadata about all tables and columns — a goldmine for mapping the database structure before targeting specific tables.

# List all databases
UNION SELECT 1,schema_name,3 FROM information_schema.schemata--

# List tables in a database
UNION SELECT 1,table_name,3 FROM information_schema.tables
WHERE table_schema='shopdb'--
users | orders | products | payment_cards

What You Need to Know

🛡️
Parameterised Queries
The complete fix. Input is passed as a parameter — never concatenated into the query string. The database treats it as data, never as SQL syntax.
🔗
UNION Injection
Appends a second SELECT to extract data from other tables. Requires knowing the column count and compatible data types in each column position.
🕵️
Blind Injection
When no output is returned — use boolean (true/false responses) or time-based (SLEEP) techniques to infer data character by character.
🗄️
INFORMATION_SCHEMA
MySQL's built-in metadata database. Contains all table names, column names, and data types — used to map the target database structure before targeted extraction.

Where SQL Injection Hides — Beyond the URL Parameter

Students learning SQL injection often fixate on the classic URL parameter injection (?id=1') and miss the full range of contexts where unsanitised input reaches a SQL query. A thorough assessment tests every channel through which user-controlled data enters the application — not just obvious GET parameters.

Example 05HTTP header injection and ORDER BY injection

Two injection contexts that automated scanners frequently miss — injecting through HTTP headers and through sort/order parameters that cannot be safely parameterised.

--- HTTP header injection (X-Forwarded-For logged to DB) ---
curl -H "X-Forwarded-For: 1.1.1.1' AND SLEEP(5)-- " https://target.com/api/data
# If the application logs X-Forwarded-For to the database without sanitisation,
# the SLEEP(5) fires on the DB server — confirming header injection

--- ORDER BY injection (column name cannot be parameterised) ---
Vulnerable code: ORDER BY " . $_GET['sort'] . " " . $_GET['order']

# Cannot use a prepared statement for ORDER BY column name
# Test injection point:
?sort=name&order=asc,(SELECT SLEEP(5))
# 5-second delay = injectable ORDER BY clause
# Fix: whitelist allowed column names and sort directions explicitly:
$allowed = ['name','date','price'];
if(!in_array($sort, $allowed)) $sort = 'name';

From Data Breach to Server Compromise — Impact Tiers

SQL injection findings span a wide range of impact depending on the database user's privileges, the database platform, and the server configuration. Understanding the full impact spectrum allows practitioners to accurately communicate severity and prioritise remediation correctly.

Tier 1 — Data Extraction

Any authenticated or unauthenticated user can extract data from any accessible table. This includes: user credentials and password hashes, PII subject to regulatory protection (GDPR, HIPAA, PCI-DSS), session tokens enabling account takeover without credentials, API keys and application secrets stored in configuration tables.

Every SQLi finding achieves at minimum Tier 1 impact — it is the baseline.

Tier 2 — Data Modification

When the DB user has UPDATE or DELETE privileges (common for web application accounts), SQLi enables: modifying account details or balances, deleting records to cover tracks, altering application logic stored in DB tables (pricing, permissions, feature flags), and inserting new administrator accounts.

Tier 3 — File System Access

MySQL with FILE privilege (uncommon but present in misconfigured environments) enables reading server files (LOAD_FILE) and writing web shells (INTO OUTFILE). This escalates from database compromise to arbitrary file read and web-accessible code execution on the server.

Tier 4 — OS Command Execution

MSSQL's xp_cmdshell stored procedure, when enabled, executes OS commands as the SQL Server service account — typically NETWORK SERVICE or SYSTEM on Windows. This is full server compromise from a web-layer injection point. CVSS 9.8+ and immediate escalation to Critical in any report.

ScenarioThe "Low-Severity" SQLi That Wasn't

A junior assessor finds SQL injection in a search endpoint on a product catalogue page. The injectable parameter returns product names — public information with no apparent sensitive content. They rate it Medium based on the data accessible through that specific injection point.

A senior reviewer notes that the injection uses UNION SELECT. They extend the test: UNION SELECT into information_schema reveals a users table in a different schema on the same database server. The same DB user the product catalogue query runs as has SELECT access across all schemas on the server. Within the users table: 47,000 customer email addresses, bcrypt hashed passwords, and full payment card data (stored in violation of PCI-DSS).

The finding is not Medium. It never was. The impact of a SQL injection vulnerability is not determined by what the specific injection point displays by default — it is determined by what the database user can access across the entire server. Always test horizontal access: what other schemas, tables, and data are reachable from the DB user's perspective, not just from the application's intended query.

Prevention — Why Parameterised Queries Are the Only Real Fix

SQL injection has exactly one reliable prevention: parameterised queries (prepared statements). Everything else — input validation, escaping, WAFs — provides supplementary defence but cannot be relied upon as the primary control. Understanding why parameterisation works where other approaches fail is essential for giving accurate remediation guidance.

How Parameterised Queries Work

A parameterised query separates the query structure from the data by construction. The query template (with placeholder markers for values) is compiled by the database engine first, establishing the query's logical structure before any user data is involved. The data values are then sent separately and substituted into the compiled structure. The engine never re-parses the data as SQL — no matter what characters it contains, it is treated as a literal value.

Secure Implementation — Four Languages
--- PHP (PDO) ---
SECURE:
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = ? AND password = ?");
$stmt->execute([$username, $password]);

--- Python (sqlite3 / psycopg2) ---
SECURE:
cursor.execute("SELECT * FROM users WHERE username=%s AND password=%s",
               (username, password))

--- Java (JDBC) ---
SECURE:
PreparedStatement ps = conn.prepareStatement(
    "SELECT * FROM users WHERE username=? AND password=?");
ps.setString(1, username);
ps.setString(2, password);

--- Node.js (mysql2) ---
SECURE:
connection.query("SELECT * FROM users WHERE username=? AND password=?",
                 [username, password])

Why Escaping Alone Fails

Many developers believe that "sanitising" or "escaping" user input — adding backslashes before quotes, stripping certain characters — is sufficient to prevent SQL injection. This approach has proven repeatedly to be inadequate for three reasons:

Defence-in-Depth: Parameterised queries prevent injection. Layer these additional controls for defence-in-depth: least-privilege DB users (web app should not have FILE, DROP, or server-admin rights); suppress verbose error messages (prevents error-based fingerprinting); deploy a WAF for additional detection and logging of injection attempts; use an ORM that parameterises by default and audit any code that bypasses it for complex queries.

Core Concepts Summary

🛡️
Parameterised Queries
Structure compiled before data arrives. Database never re-parses user input as SQL. The only reliable fix. Escaping is error-prone and context-dependent — parameterisation is structurally correct.
🔗
UNION Injection
Appends second SELECT. Requires column count match (ORDER BY enumeration) and compatible types. NULL works as a universal type placeholder. Always extend to information_schema to understand full accessible data scope.
🕵️
Blind Injection
Boolean: page changes on true vs false condition. Time-based: SLEEP(5)/WAITFOR confirms injection when page is identical. Both extract data without visible output — slower but functional against suppressed-output applications.
🗄️
INFORMATION_SCHEMA
MySQL metadata: schemata (databases), tables, columns tables. Always map the full database structure before targeting specific data. Other schema tables may be accessible beyond the application's intended scope.
📍
Injection Contexts
GET/POST params, HTTP headers (X-Forwarded-For, User-Agent, Cookie), REST path segments, search LIKE queries, ORDER BY parameters. Test all input channels — header and ORDER BY injection are frequently missed.
📊
Impact Tiers
Tier 1: data extraction. Tier 2: data modification (UPDATE/DELETE access). Tier 3: file system (MySQL FILE privilege). Tier 4: OS execution (MSSQL xp_cmdshell). Impact is determined by DB user privileges, not by what the default injection point displays.
🔍
Comment Characters
MySQL: -- (space) and #. MSSQL: -- (space). PostgreSQL: -- (space). The comment terminates the rest of the original query after injection. Essential for auth bypass — commented-out password check = login with any input.
⚠️
Horizontal Access
Always test what the DB user can access across the entire server, not just what the specific injection point displays. A product catalogue injection running as a DB user with cross-schema SELECT access may reach customer PII in a different schema.
Ready to put it into practice?
Proceed to the Lab

You've covered the theory. Now apply it hands-on in the simulated environment.

Start Lab — SQLi
← Return to all labs