Let’s get into the nitty-gritty. The only way you can reset your password on Facebook (if you’ve forgotten one) is through entering a 6 digit passcode. Well that’s 10⁶ = 1,000,000 possible combinations. Some algorithm which Facebook uses (that is yet to be cracked) generates seemingly a random 6 digit code whenever a person requests a password reset. That code does not change if you request it from mbasic.facebook.com until that code gets “used.” That could possibly mean that if 1 million people request a password within a short amount of time such that no one uses their code to reset the password, then 1,000,0001 person to request a code will get a passcode that someone from the batch has already been assigned.
There are 2 options here: 1) Facebook either stores duplicate codes for multiple users if more than 1 million people request a password reset code, or 2) Every user gets a unique code and Facebook uses some divine way to handle the case where 1 million+ users request a code. Since I don’t know much about the divine, I put my money on option 1.
Hence, I decided to send double the number of emails (2 million of them),hoping that some people from my 2 million will get duplicate passcodes. This is a simple application of the Pigeonhole Principle. Then all I have to do is pick a random passcode following this rule: Integers less than 100,000 have a lower probability of occurring than integers between ranges of 300,000 and 699,999 or 800,000 and 999,999, which have higher probability of occurring. Again, this isn’t the golden rule of thumb but from my testing it will help us later. So now that we have picked a random passcode, we will brute force it against our 2 million batch to check whose ID is associated with our random passcode!
The bug isn’t difficult to understand but it’s execution is tough due to its large scale.
How do you send 2 million password reset emails quickly without getting blocked?
To send emails, you first need to get access to 2 million Facebook usernames. Web scraping time!
Point 1: Facebook IDs are generally 15 digits long, so I started with 100,000,000,000,000 and started making queries to Facebook Graph API to check which IDs were valid. I was also able to get profile picture and full name on the user’s account with ease since it seems there is no rate-limiting on public data (I just did it for fun). But wait! Facebook Graph API only lets authorized apps to fetch a user’s username, doesn’t it? Yes it does. Yes it does.
All you have to do after making sure the ID is valid is visit the following link:www.facebook.com/[ID HERE] and the url automatically redirects and changes the ID to the user’s username. So I compiled all this data into a nice JSON, which I guess doesn’t hurt to publish since it’s all public anyway.
Note: Some of the profile picture urls in the JSON are invalid.
Point 2: In order to avoid getting your IP blocked from repeatedly sending requests to send password reset emails, you need rotating IPs. This means that every email request will be sent from a batch of thousands of IP addresses to simulate a normal global network flow. There are several services online that offer this feature. In my case, all network traffic went through a proxy server that listened for HTTP requests and arbitrarily assigned an IP address to each request.
Point 3: You need to simulate user behavior when requesting a passcode. So we will use PhantomJS (Headless browser) and write a multithreaded script in Java that requests a passcode to every user from our JSON file. I also scraped all User Agent strings for a Chrome browser fromhttp://www.useragentstring.com/pages/useragentstring.php?name=Chrome to assign to my PhantomJS instance.
Point 4: Got a free trial of Google Compute Engine and hosted my scripts on a virtual machine. I set up 8 VMs (12 cores/20 GB RAM each) over 4 different regions and instantiated 180 PhantomJS instances per VM for full CPU utilization. Then I let all my scripts do their thang!
I could’ve created a distributed system for my VMs but time is money.
Easier Part: Brute Force Guessed Passcode Against 2 million IDs.
I then guessed a 6 digit passcode 338625 using the aforementioned rule and brute forced all users at the following url by adding the ID to the key ‘u’ and my passcode to the key ‘n’: www.beta.facebook.com/recover/password?u=…&n…
And guess what? I was able to find a matching ID.
I again went to www.beta.facebook.com/recover/password?u=[IDHERE]&n=338625 and I was brought to this page below
Now you get complete access to that random user’s account.
Bounty Paid: $500
At it stands, this critical bug which lets you gain complete access to someone’s account is Facebook’s low priority (don’t know why).