Saturday, 6 December 2014

Reading local files from Facebook's server (fixed)

Hello,

Recently I found a vulnerability in Facebook which allowed me to read local files from Facebook's servers. The vulnerable part of Facebook was their Careers resume uploader, located at every job offer, for example this one.

You can upload any extension there, so I decided to upload a .php file :-) - one can always hope. Of course, it was not executed nor could I get file path, but contents of the file were returned, after being base64 encoded. Next thing I tried was to name my file /etc/passwd, "file:///etc/passwd" and couple others. None of these worked.

Couple tries later I uploaded a zipped .php file, and the response contained unzipped, base64'd contents of .php. If you read Facebook's "Bounty hunter's guide" you will know where this leads.
The guide describes how one researcher uploaded zip with symlink to /etc/passwd, and couple steps later Facebook returned few lines of /etc/passwd.

I have done exactly the same, so:
1. create a symlink to /etc/passwd (or any other file you want to read)
ln -s /etc/passwd link
2. zip the created link while preserving symlinks:
zip --symlinks test.zip link
3. upload test.zip as your resume, system will unzip it
4. the response to POST will have details of (whole) /etc/passwd or other file. 

Here is a screenshot of response containing /etc/passwd:

I have speculated about symlinking a directory, but never tried it. Neal from Facebook thought that this might get me contents of files from the directory, but not necessarily filenames. We shall never know. 

Here is a timeline of the bug report:
Nov 30, 2014  09:45 - vulnerability reported
Nov 30, 2014  17:58 - reply from Facebook's security (Neal) saying they cannot reproduce bug
Nov 30, 2014  18:08 - update from Neal, they can reproduce it
Nov 30, 2014  19:10 - temporary fix has been pushed, disabling resume uploads
Dec 01, 2014 ~23:00 - more permanent fix pushed, now server no longer responds with contents of uploaded resume (Emrakul)
Dec 05, 2014  18:15 - bounty awarded (Neal).
Dec 05, 2014 ~19:00 - objection about reward sent to Facebook's team
Dec 06, 2014 ~23:30 - Neal from Facebook explains this is actually a third party system they run

I'd like to ramble a bit about the award for this bug. When I found it, it looked like a critical bug that could allow me to read parts of Facebook's source. It turned out to actually be a third party software they used to analyse uploaded resumes, therefore I could not actually access any part of Facebook's internals. The network this system is hosted on was pretty locked down, too.

Basically, this is a bug that looks really critical, but is much lower severity. It is an exact opposite to a low severity bug I found some time ago that turned out to be more dangerous.

Lessons learned:

  • Read all write-ups you can get your hands on. I would never think of uploading the zip with symlinks if I did not read Facebook's blog. Philippe has a good list of Facebook bugs here. 
  • Some bugs are not what they look like :-)
Huge thanks to researcher who first used ZIP with symlinks, and to Facebook for blogging about it, and their security team for being awesome as always, fixing the bug in 10 hours on Sunday.

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