// Security · SQL Injection
SQL Injection in Logs
SQL injection payloads are sent through URL parameters, POST bodies, cookies, and HTTP headers. The access log captures the URL and query string. POST body data is not logged by default but the URI path and any GET parameters always are. Here's how to find SQLi in your logs.
SQLi Log Examples by Technique
Union-Based (most visible in logs)
# sqlmap automated union probe — note URL-encoded spaces (%20) and comment (--) 91.108.4.12 - - [10/Apr/2025:04:12:33 +0000] "GET /products.php?id=1%20UNION%20SELECT%201%2Cuser()%2C3%2C4-- HTTP/1.1" 200 4821 # Same attack with + for spaces (also common) 91.108.4.12 - - [10/Apr/2025:04:12:34 +0000] "GET /products.php?id=1+UNION+SELECT+table_name,2,3+FROM+information_schema.tables-- HTTP/1.1" 200 9240
Boolean Blind (slower, less obvious)
# True condition — returns normal page 198.51.100.9 - - [10/Apr/2025:04:15:01 +0000] "GET /item.php?id=5+AND+1=1 HTTP/1.1" 200 3120 # False condition — returns empty/error page. Different byte count = vulnerable 198.51.100.9 - - [10/Apr/2025:04:15:02 +0000] "GET /item.php?id=5+AND+1=2 HTTP/1.1" 200 312
Error-Based
# Triggers a MySQL error to leak data in the error message 203.0.113.77 - - [10/Apr/2025:05:00:12 +0000] "GET /search.php?q=test'%20AND%20extractvalue(1,concat(0x7e,version()))-- HTTP/1.1" 500 891 # 500 + this pattern = error-based SQLi confirmed if MySQL error appears in response
Time-Based Blind (hardest to spot in logs)
# SLEEP() — if vulnerable, response takes 5 seconds. Check $request_time in Nginx logs 203.0.113.77 - - [10/Apr/2025:05:01:00 +0000] "GET /api/user?id=1;SELECT+SLEEP(5)-- HTTP/1.1" 200 220 rt=5.013 # Benchmark alternative (avoids SLEEP keyword filtering) 203.0.113.77 - - [10/Apr/2025:05:01:06 +0000] "GET /api/user?id=1+AND+BENCHMARK(5000000,MD5(1))-- HTTP/1.1" 200 220 rt=4.891
Stacked Queries / Second-Order
# Attempting to inject a second SQL statement (works on some DB drivers) 198.51.100.9 - - [10/Apr/2025:05:10:00 +0000] "GET /user.php?id=1;DROP+TABLE+users-- HTTP/1.1" 500 443
Encoding Tricks — Same Attack, Different Forms
Attackers encode payloads to evade WAFs and simple string matching. All of these are the same UNION SELECT payload:
| Encoding | In Log |
|---|---|
| Plain | UNION SELECT 1,2,3 |
| URL encoded | UNION%20SELECT%201%2C2%2C3 |
| + for spaces | UNION+SELECT+1,2,3 |
| Double encoded | UNION%2520SELECT%25201%252C2%252C3 |
| Case mixing | uNiOn SeLeCt 1,2,3 |
| Comments | UN/**/ION/**/SEL/**/ECT 1,2,3 |
| Hex | 0x554e494f4e2053454c454354 |
💡 Use -i flag
Always use grep -i (case-insensitive) when scanning for SQLi. Attackers routinely mix case to bypass naive string matching.
Detection Commands
bash
LOG=/var/log/nginx/access.log # Comprehensive SQLi pattern scan grep -iE "union.{0,20}select|or\s*['\"]?\s*1\s*=\s*1|--[\s+]|%27|%2527|information_schema|extractvalue|benchmark\(|sleep\([0-9]|load_file\(|into\s+outfile" $LOG # Filter only those that got a 200 (most dangerous) awk '$9=="200"' $LOG | grep -iE "union.*select|information_schema|extractvalue|load_file" # Detect time-based (Nginx only — requests taking > 3s with SQL keywords) grep -iE "sleep\(|benchmark\(" $LOG | awk -F'rt=' 'NF>1 && $2+0 > 3' # Identify sqlmap by User-Agent grep -i "sqlmap" $LOG | awk '{print $1}' | sort -u # Count SQLi attempts by source IP grep -iE "union.*select|or.1=1|sleep\(" $LOG | awk '{print $1}' | sort | uniq -c | sort -rn
🚨 If You See SQLi with 200 Responses
A 200 response to a UNION SELECT or information_schema query means your database driver processed the injected SQL and the page returned data. This is an active breach. Immediately: take the site offline, preserve logs, check the MySQL general query log for what data was accessed, rotate all DB credentials.