Friday 5 September 2014

Step-by-step: exploiting SQL injection(s) in Oculus' website.

Hello,

Some time ago Jon of Bitquark tweeted that he found a SQL injection and RCE in one of Facebook's acquisitions.
You can find Jon's blog about the RCE vulnerability here.

I guessed it was Oculus' website and started looking for more SQL injections and other vulnerabilities, resulting in a total of 5 SQL injections and a couple smaller vulnerabilities.
Of those 5 injections, two were duplicate.

I am not going to write about all 5 of them, but the one I have exploited to gain administrator user:
SQL injection in developer.oculusvr.com/core/CompanyAction.php
A POST parameter "Domain" was vulnerable to SQL injection, but there were few problems that made exploitation harder.

Problem one: Syntax error was needed to differentiate true from false. 
The usual
'and'a'='a
did not work and I needed to get syntax error for false.
Lets build the basic true/false check:

Domain='and (select 1 union select case when (1)=1 then 1 else 2 end) and '@test.com

So, when 1=1 then the subquery looks like (select 1 union select 1) which is a valid query.
Now, for 2=1 the subquery would be (select 1 union select 2) and it is an error, because subquery returns more than one row.
We have the basic idea for true/false responses now.

Problem two: No whitespaces.
Oculus' domain filter required domain not to have whitespaces. Lets change the query to bypass this. I have decided to go with
combination of comments /**/ and parenthesis. So:

Domain='and(select(1)union(select(case/**/when(1)=1/**/then/**/1/**/else/**/2/**/end)))and'@test.com

Great, this works too!

Problem three: No commas. 
This one is a bit tricky. For blind injection we generally need LIMIT, substr() and other functions that need a comma. Well, they actually do not. After going through MySQL docs I saw comma-less syntax is possible for both LIMIT and substr():

LIMIT 0,1 is same as LIMIT 1 OFFSET 0
substr('Hello',1,1) can be used as
substr('Hello' from 1 for 1). 
Here is the query to get database column name - character by character:

'and(select(1)union(select(case/**/when((select/**/ascii(substr(column_name/**/from/**/1/**/for/**/1))/**/from/**/information_schema.columns/**/where/**/table_name/**/like/**/'xxxxxxxxxxxxxxx'/**/limit/**/1/**/offset/**/0))=97/**/then(1)else(2)end)))and'@test.com

I have guessed the table name, and parts of column names for sessions table using LIKE and a bit of luck.

Problem four: it is blind injection, therefore slow. 
I did not use sqlmap for this, but I did help myself with Burp's Intruder. Since the goal was admin's PHPSESSID and it is [0-9][a-z], I simply set Intruder to bruteforce using that character set until it finds a true response, and then manually took out the characters and assembled a PHPSESSID. It was still slow, but okay - around 10 minutes on average to get the whole 26 characters long string. It would be faster but I actually needed to do another request to see if response is true/false - you'd first get 302 redirected, then see the response. Because of this I had to use one thread only and a small delay between requests, around 0.3 seconds.

Using the PHPSESSID I logged in to admin panel. Jon's way to get command execution still worked, the panel was also vulnerable to more SQL injections. I have started reporting all those, but was asked to log out - which I did. I have also tried to get RCE using file-uploads, but as far as I know it did not work.

That is all folks, thanks for reading :-).

I would like to thank Jon of Bitquark for sharing knowledge, Oculus for not using prepared statements, and Facebook for running the bounty program.
The vulnerabilities were fixed in a few days.

Oh yeah, most of this stuff I learned with/from a CTF team - you can find their blog here and their security challenges here