Hi!
Today, I want to share with you a write-up about the two account takeovers I presented on as a speaker at the Bug Bounty Argentina Village during Ekoparty 2024.
I’m taking the opportunity to outline the technical step-by-step for each one, adding a few more details along the way. I’m detailing my approach specifically for those who had questions at the end of my presentation, those who came to chat with me afterward, and for the entire bug bounty community.
I’d also like to take this chance to thank Bugcrowd for supporting me and sponsoring my trip to give this talk.
A special thanks to Link Clark, who values my input, not only in the projects he’s working on, but also in supporting my own. Thank you, Link!
Let’s get started!
Non-brute force account takeovers: OAuth and 16-digit code manipulation
Let me tell you how how the register worked via Oauth on www.vulnerable.com. My first steps include opening Burpsuite, configuring the scope, and activating the proxy so that all requests and responses are stored in the Burp history. That way, I can review the entire login flow later.
Next, I review the option to log in via Google. If I don’t find anything interesting there, I move on to review the option to register via Facebook (FB).
I enabled login via FB, meaning I gave permissions in the FB app to enter through. In other words, I gave permission to www.vulnerable.com to read the data from my Facebook account and thus obtain a user on www.vulnerable.com. Then, I logged out and logged back in. There, I found this POST:
Login request
As you can see, it’s a POST request to the application’s API and it sends a JSON with five values in the body, of which only two are important: loginType and oauthId. Other than those two values, the rest can have a random value and nothing happens. This means it accepts the request without any problems.
Note: I always try removing values to understand what is important in that request. I want to know what is needed and what is optional. This is valid for JSON values, parameters in GET, or cookies.
Pay attention to the second value. Notice that it’s peculiar that the value is not a FB token, but the FB userId. More interestingly, this userId is not the global FB ID. When an APP is allowed to link to FB to share information, each app has an APPID and a USERID that varies in each linked app. In other words, it’s a unique number that FB provides to the user of that particular app and it doesn’t repeat for any other app.
Login response
To my surprise, the authentication method only needed the previous POST with a valid oauthId in the body. The entire login security relied on the oauthId. Yes! Inputting the 16-digit number of that unique number that FB provides, the api responded with valid session cookies for that user:
Current state of affairs
So I said to myself, “bronxi, it’s 16 digits. Brute force could be possible if I copy the obtained cookies and access the account of any user of www.vulnerable.com that has their account associated with Facebook.” But, brute force? The program didn’t say anything about prohibiting brute force. Do I report it or keep looking? My dilemma was whether I report it as is, using brute force or not. If I did this, I ran the risk of being told by the program owner that if brute force is required, it’s not valid. So I said to myself “bronxi, calm down, focus, and keep looking for a way to get that Facebook userID.”
Findings with automation
In this specific program, I had completed my reconnaissance work ahead of time. During my recon, I obtained many parameters with paramspider. I used those parameters and the fuzzing templates of @0xKayala (templates) to run nuclei. There, I found some XSS, but I didn’t report them because in the policies of the www.vulnerable.com program, they made it clear that they do not accept XSS.
But maybe, if I used one of those XSS findings to obtain the Facebook userID, they might accept it. This possibility might work because the vulnerability itself is the account take over and I would use another bug for it: an XSS. The XSS itself is not the vulnerability in this scenario. So, I started checking the storage in the browser to understand how it stored the FB userID.
LocalStorage & XSS to my server
I found the FB token in localStorage in a value called fblst_666450503370175. You read that right, yes, the token! Not the user ID that I was looking for.
I used one of the reflected values that I found with nuclei and after several tests, I came up with a payload that worked correctly. It sent the value stored in localStorage to my server:
However, since the implementation of authentication via FB was incorrectly configured with the userID, it did not use the FB token which would be correct. So, even though I got the token, I couldn’t log in with it. This struck me as funny. I had the FB token but it didn’t work to log in, I needed the 16-digit userID. So I got more critical information, like an auth token. Unlike a userID, and due to an incorrect configuration, the token was not used; only the userID was used.
Plan A: Report it
I pondered and said to myself, I’ll propose Plan A using brute force. I’ll mention that I could exploit it in a targeted way to a certain user using XSS to obtain it, although I still hadn’t figured out how to link the FB token with the userID. I reported it like this but got an N/A. The response, which makes sense, was that a brute force attack on 16 digits was not a reasonable attack. That gave me a dose of motivation to exploit it in a targeted way with XSS.
Plan B: XSS
I launched Plan B at the risk of being told by the program owner that it doesn’t apply because I’m using an XSS. But since I got the FB token and not the userID, I couldn’t exploit it. So I started investigating if it was possible to obtain the userID through the FB token. I found the FB API, where there was an endpoint that you sent the user’s token to via GET parameter and it responded with the userID 🙂
My reproduction steps for Plan B:
- The attacker sends a malicious link to the victim to obtain their Facebook token stored in localStorage.
- The victim receives that malicious link and opens it.
- The attacker receives the Facebook token on their server.
- The attacker uses the Facebook API to obtain the User ID. https://graph.facebook.com/me?fields=id&access_token=XXXXXX
- The attacker uses the userID to obtain the victim’s session cookies and logs into the victim’s account.
Deleting tokens and linking UUIDs
I started investigating this when I came across an endpoint called /auth/update-password during my tests.
To change your password, the FB platform sent a POST request. The body of this included the parameters:
- “User”—which contained a UUIDv4 identifying the user
- “Password”—which specified the new password, and
- “Token”—which provided security for the password update below
One of the tests I performed was deleting the token from the body and sending it like this:
To my surprise, I received a response with the status code 409 and the user’s data. I tried logging in with the previous password and it didn’t work, but when I tried with “OtherPassword$1”, the password had indeed been changed. The “user” identifier, which as you can see is a UUIDv4, could be seen each time I logged in. The next problem for me to solve was how to obtain the “user” identifier for any other victim user.
Next, I tried removing the “user” parameter and replacing it with “email” but I got an error in the response.
I considered several pathways for my next tests. One was to look for URLs revealing the UUIDv4 that identifies users in services like Wayback Machine, but I had no luck. So, I tried the next best option: what happens if I invite a user who already has their email registered to my workspace?
I created two workspaces, one for the attacker (attacker@bronxi.io) and one for the victim (victim@bronxi.io). From the attacker’s workspace, I went to the invitations section, wrote the victim’s email (victim@bronxi.io), and invited them. In the response, I received the UUIDv4 that identified the victim. However, it was different from the UUIDv4 I could see in the victim’s workspace. This meant the platform assigned a different UUID for each workspace even if it was the same email.
There was something strange here. I accepted the invitation as the victim and was taken to the workspace where the attacker had invited the victim. I logged out and logged back in. When I logged back in, I entered the victim’s original workspace. Here was another bug that by itself wasn’t a security issue, so I continued pulling the thread.
I tried changing the password using the UUID obtained after the invitation in the attacker’s workspace, but the response was that it didn’t recognize that user. So, I understood that to change the password, I had to use the UUID generated in the victim’s original account. Therefore, the UUID generated in subsequent invitations couldn’t be used to exploit that bug.
I decided to check what would happen if I went the route of “forgot password” for the victim user (victim@bronxi.io). After updating the victim’s password following the normal forgot password process, receiving the email with the URL, and changing the password, I logged in again and this time was taken to a dashboard where I had to choose which workspace to enter: the victim’s original one or the attacker’s workspace where the victim had been previously invited.
Again, I tried using the victim’s UUID obtained within the attacker’s workspace. I used the POST request for update-password and to my pleasant surprise, it did change the victim’s password!
But this whole flow didn’t add up. For this account takeover to work and for the new UUID assigned in the exploitation to work, the victim had to reset their password after the attacker invited them to their own workspace. In previous tests, the platform had locked me out of accounts after failed login attempts. I conducted a test specifically to find out how many attempts it took to lock the account: six attempts. After six attempts with an incorrect password, the account was locked and the user had to perform a “forgot password” to regain access to their account. The attack flow was complete!
Steps to reproduce:
- The attacker creates a workspace and invites the victim’s email address as a user of the workspace (victim’s acceptance of the invitation is not required).
- The attacker attempts to access the victim’s account six times with any password to lock the victim out.
- When the victim attempts to access their account, they receive a message stating it is locked and proceeds to perform the password recovery action. They receive the email and reset their password.
- The attacker copies the victim’s UUID found in their workspace.
- The attacker uses this UUID in the request shown below to change the victim user’s password:
- The attacker can then access the victim’s account with the new password. The victim has lost access.
In this step, you will notice that both the victim’s workspace and the workspace to which they were invited by the attacker appear, even if the invitation has not been accepted. This occurs when resetting the password.
Two important considerations
- A link occurs when the victim resets their password through the password recovery link. This link is formed between the UUID of the victim’s pre-existing account (where they have their own workspace) with the new UUID generated upon inviting them. At that moment, the UUIDs are linked. I have a hypothesis regarding this: since it’s the same email when querying the database, the first UUID is taken as the primary one when resetting the password, which is the one from the invitation made by the attacker. Thus, both UUIDs are linked to the same email, i.e., the same account.
- After the attacker has performed the account takeover and the victim notices their password doesn’t work, the victim can perform a “Forgot password” action. However, the attacker can change the password again as many times as they want WITHOUT NEEDING to redo all the steps. They only need to perform step 5 and nothing else.
I hope you enjoyed them. You can follow me for more tips and clips of my presentations at my YouTube or my LinkedIn
Best!