I spoofed access to other people’s email in order to pre-steal user accounts before they are first registered. Here’s how I did it.
One thing I always test while hacking on bug bounty programs is how applications generate tokens. Tokens are used for things such as password resets, email address verification, one-click sign-in, etc.
While hacking on one private program I discovered an application with a weak algorithm for generating email verification tokens. This is how I approached the problem and how I found a way to generate valid tokens to verify ownership of any email address, even those I didn’t actually have access to. This enabled me to pre-register accounts on any email address with a password of my choice which leads to an account takeover when the real owner eventually registers.
Analysing The Token Generation Algorithm
The first thing I test while attacking any web application with a bug bounty scope is the account creation and authentication functionality. On this occasion, I discovered that each time I created a new user account, the application wouldn’t let me do anything until I had verified ownership of my email address. The target application would email me a verification link and prompt me to check my inbox and that was as far as I could go
By forcing me to validate my email address the application owners can prevent pre-account takeovers.
Modern web applications often have multiple ways to log in to a user account. These can include:
- Username and password
- Email address and password
- Oauth such as ‘Sign in with Facebook’ or ‘Login with Twitter’
- SAML Single Sign-On from any SAML compatible Identity Provider (IdP)
- Passwordless using tokenised ‘magic’ links
A pre-account takeover is when an attacker creates a user account with one login method and then the victim creates another account with another login method. The application then links the two accounts together based on the matching email address. When the email address is not validated, or validations are bypassed, this can lead to pre or post account creation takeovers.
The attack happens as follows:
- The attacker creates a user account on a web application using an email address that they don’t own.
- The application doesn’t verify that they are the owner of the email address
- The account sits dormant for a period of time
- The real owner of the email address chooses to ‘sign-in with Facebook’ and Facebook provides the users validated email address.
- The Facebook email address matches the email address the attacker signed up with and the two accounts are linked.
- Now the attacker can log in to the victim’s account with an email address and password that they supplied and they can see everything the victim does inside their account each time they log in with Facebook.
Email Verification Token Recon
Checking my inbox, I found the ‘confirm your account’ email and copied the link into my favourite text editor. I always save all one-time links emailed to me along with the account name, email address, registration time, and so on, in case any patterns emerge while testing.
The link had the format https://craighays.com/verify/?P1=randomString &P2=randomString &P3=staticText &P4=staticText (without the spaces).
After generating a few verification emails for different accounts I could see that P3 and P4 were always the same things. P1 and P2, however, always changed each time an email was sent. Opening the link after removing or altering P1 or P2 resulted in an error message and the email address was not verified. Therefore, the combination of P1 and P2 together formed a two-part email address ownership verification token.
Now that I had identified the two-part token used to confirm an email address, I took a deeper look at the two parameters.
Both P1 and P2 were base64 encoded strings. An easy way to spot this is when a random-looking string ends with a single = or double ==. These = chars are used as padding to ensure the pre-encoded string meets the minimum bit length. This isn’t always the case though as a perfect-length string won’t need padding. If in doubt, always pass random strings through different decoders to see if anything interesting comes out.
After decoding multiple P1 and P2 parameters from different links I could see that P1 always had the format of four static characters followed by 12 numerical characters such as TEXT000123456789 and P2 always had the format of 9 numerical characters such as 123456789. Looking through my list of decoded P1 and P2 strings, I could see that the numbers were incrementing, going up in large steps from one email verification link to another. P1 and P2 were not correlated in any way other than that they both went up.
At first, I tried to guess the numbered strings based on the timestamps in the HTTP response, but that was getting me nowhere. Instead, I had the idea of defining upper and lower bounds within which a valid token for an email address would reside. With a small range, even a 9 or 12 digit number could be brute-forced to create a valid token.
Upper and Lower Bounds
Looking at the problem mathematically, I needed to guess two 9 digit numbers. The twelve-digit number always started with 000 so it was effectively 9 digits long. A 9 digit number has 1,000,000,000 possible combinations. Since I have two of them, I have 1,000,000,000 * 1,000,000,000 possible combinations for each email verification token. Pretty secure.
The issue with this application was that in each token generated, P1 and P2 had numbers greater than the previous P1 and P2, but lower than the next P1 and P2. If I can create a token that I can see, followed by one I can’t, then a third that I can see, I can reduce the range of possible numbers significantly. I can create upper and lower bounds on the 9 digit number which is a much smaller range than the 1,000,000,000 possible combinations.
The step by step of my token attack looked like this:
- Create an account with an email address I own
- Create an account with the email address I don’t own, but want to validate
- Create an account with an email address I own
Putting This Into Practice
In order to prove that I could validate any email address, I decided to validate my own name at the target company domain, e.g. [email protected]
I opened up three browser profiles and took the account creation process to the last page on all three browsers. As quickly as possible, (using burp intruder to hold the requests before releasing one after another) I submitted the ‘create account’ form on all three browsers with [email protected] in the middle. This sent me two emails with an email verification link in each. The middle email went to an address I didn’t have access to.
Extracting the P1 and P2 parameters from the two verification links, my upper and lower bounds looked like this:
P1 Range: 39
P2 Range: 210
Possible combinations: 8,190. Much better than 1,000,000,000*1,000,000,000 = 1.0e18
Then I ran the following logic in python :
for P1 in range (LowerP1..UpperP1):
for P2 in range (LowerP2..UpperP2):
P1 = "TEXT000" + string(P1)
P1 = base64encode(P1)
P2 = base64encode(P2)
url = buildURL(P1, P2)
# Note, this is pseudo code - it won't actually run but
# it summaries the script logic into a readable form
As part of buildURL() I added in P3 and P4 and the rest of the URL. Then I simply sent an HTTP GET request of all 8,190 possible 2-part tokens between the upper and lower bounds until my email address was validated.
With this, I was able to validate [email protected], proving that my verification bypass worked. As the core functionality of the web application already linked email and password created accounts with Facebook created accounts with a matching email, I didn’t need to prove that any further. A simple explanation of the behaviour is all that was required in my bug bounty report.
As a by-product, not only did it validate the account I created, it also validated a few other email addresses created by members of the public who were within range of my upper and lower bounds at my time of testing… oops.
I was able to bypass the account verification requirement and use the application without being tied to a real email address. I was also able to run pre-account takeovers on any email address that wasn’t already registered, ready for the first time an unsuspecting victim logged in with Facebook, Twitter, or whatever for the first time.
The best way to fix this was to replace P1 and P2 with a really long and random string of characters, removing sequential numbers from the token generation algorithm. Weirdly, the password reset method already did that but for some reason, the developers reinvented the token process for validating email addresses.