BLOG
- Posted on: Nov 17, 2022
- By Vineet Nair
- 13 Mins Read
- Last updated on: May 9, 2024
Imagine this situation. You were using a very well-reputed, billion-dollar mobile application. You are having a good time with the application and its services. Suddenly you go to the login system, and the application tells you to provide your phone number to proceed. You enter your phone number as any normal person does. The application then asks for an OTP sent to the phone number you entered. Out of nowhere, your security researcher personality kicks in, and you say, "What if I can brute force this four-digit OTP and crack any account?". Well, you enter 10-15 wrong OTPs, and the app doesn't lock you out, and then you enter your correct one, and the application accepts it, and you are logged in. You are filled with joy and pleasure because you found an authentication loophole, and you are on your way to getting that bug bounty. You start Burp Suite, connect the application via the proxy, get ready with the Intruder, try to relay the traffic, and then the login process looks like this.
You send a couple of requests, but nothing seems to be working. It's the same login verifier thing you keep getting, and you cannot proceed from here, but the app appears to login pretty well with no issues. You lose all your motivation and dreams about buying that PlayStation 5 with your bug bounty money just disappearing like Harry Houdini. Suddenly you found my blog, and all your doubts disappeared, and now you could easily get that bug bounty. It could still be possible. Well, what you saw in the above request is known as an OAuth flow. Sit tight because I will explain how I found a brute force vulnerability in one of the most popular applications while working in Appknox.
Also, the story I wrote above was real, and I had my frustration while dealing with this thing (minus the bug bounty and PlayStation 5). Anyways let's get started.
What is OAuth?
OAuth, which stands for Open Authorization, is an open standard for access delegation. It is commonly used for internet users to grant websites or applications access to their information on other websites without giving them passwords.
Imagine you took a wonderful photo of yourself on a beach (because everyone likes beaches, right?). You saved it to google photos, and now you want to edit it. Unfortunately, the editor of google photos does not have the advanced filters to hide all those dark circles you might have. So you use a Powerful AI photo editing tool to do those advanced editing. The editor tells you that it needs access to your google photos to load them in its editing system.
Before OAuth, the only way you could do it was to provide your username and password to the service you are trying to use (in this case, the photo editor). The service would use those credentials to log in to the website to access your data and then fetch the necessary stuff it needs to operate.
In this example, you would provide your google account credentials to the photo editor. The editor would log into your Google account, access Google photos, and load the image you want to edit.
Well, this process worked, but it had severe problems. First things first, are you comfortable providing access to your entire google account, which includes not just photos but your Gmail, drive, youtube, etc.? No, right? That was one of the significant problems. The second issue was what happens if your service gets compromised. All the credentials stored by the service will be leaked, leaving the entire user base vulnerable.
OAuth was developed to counter the same problem. Well, you might ask, "How does this standard help with the above problem?". Well, in OAuth, you don't have to enter your credentials in the service you are using. You might have seen this dialog box when you try to use any service with google.
This is known as a consent screen (we will look at this later). When you click "Allow," the application magically gets access to the requested resource. How does this work? Ok, so unlike the older method where you enter credentials, this time, the authorization service of the resource server (google photos, in this case) handles this for us. As soon as you provide the authorization grant to a service to access your Google photos, Google's authorization servers provide an access token that is scoped to be only used in Google photos and nowhere else. The token also has permission as to what the application can do with it; for example, can it only read the photos or maybe even save them back to the drive? Since an editing application only wants to access the photo (as the edited photo can be downloaded directly), the token will only have a "Read" permission.
In this way, even if the token gets compromised, the damage done by a potential attacker will be locked down to the Google photos application. Even this compromise is very difficult as each token has an expiration time, too, but anyways that's beyond the scope of this blog.
We will go more deeply into this process in the next section, but for now, this is all we need to understand what OAuth is.
OAuth Working
Let's look at the image from the introduction section and pick apart what those long request parameters are.
Let's look at them one by one. Trust me; it's not that hard, it just looks intimidating. If you see below, we have around 11 parameters in the request.
redirect_url
This parameter tells the application who's utilizing the OAuth process where to redirect after each successful OAuth flow. In this case, you can see the URL is com.app.auth:/oauth_callback. So, in short, this is an android deep link, which technically means it's handled by a native intent or service inside the application.
client_id
This parameter is an identifier for that client communicating to the application's OAuth servers. These are generated and managed by the app developers from the backend.
response_type
This parameter defines the type of data exchanged after a successful OAuth process. In this instance, the data type is "code," meaning that a code will be sent to the client, who will then need to exchange it at an API endpoint for an access_token or a bearer_token.
state
This parameter is a secure random arbitrary value commonly used to mitigate CSRF attacks while an OAuth flow is in process.
scope
This is basically used for managing the permissions of the token that will be granted after the OAuth process completion. In this case, this is the payload sent in the request. "openid offline identities.read identities.update accounts.read". OpenID is the identity provider, and the read and update permissions are given to the final token.
code_challange
This is a bit complex to explain right in this way, but I'll still try my best. So the code challenge is technically a part of something known as PKCE, which stands for Proof Key Code Exchange. It's an extension to the client authentication model, which protects the OAuth system against CSRF and authentication injection attacks. I will dive deeper at the end when we write the exploit for brute forcing this by hand via python, but for now, understand this is used so that the authorization server knows that the client who is exchanging the authorization code is the same one who actually requested it.
That's it for now. I'll explain this in the later part. Trust Me!
code_challange_method
This parameter is the hashing method which was used to derive the code_challange from the code_verifier. code_verifier is simply a secure random string (it's a part of the PKCE process), which is then used to acquire a code_challenge by hashing it using SHA-256. The code_challange_method here is set to S256, which simply means SHA-256 hashing algorithm.
Again, trust me; I'll make this simpler to understand in the exploitation scenario.
verification[otp]
This parameter is simple. It's the actual value of the OTP that the user is sending for a successful login. So by now, I guess you know what our attack parameter is!
verification[nonce]
This is a parameter that is substituted with a UUID-4 like value while the OTP request endpoint is triggered. To be honest, I never really understood the real function of this parameter, but I feel like this is to mitigate things like reply attacks.
verification[method]
If you look at the parameter value, it's set to "OTP". It simply means the method of verifying the user is an OTP sent to the user's entered phone number.
verification[phone]
This is the phone number to which the OTP is sent to. Normally this is the user's phone number where they want the OTP to be sent.
nonce
Again this didn't make much sense to me, but I feel like this is again something used to mitigate things like request replay attacks. Even though I could replay the entire request repeatedly with the same nonce 😂.
Oof!!😅 That was a lot of parameters, man. Don't worry though; we don't need to memorize or work with all of them to exploit this endpoint. If you look closely, there is a cookie too, which is named "oauth2_authentication_csrf". This is an important thing too. We will look into this in the OAuth Flow section of this blog, where we walk through the entire flow in Burp Suite step by step but for now, keep it in the back of your mind that we have 1-2 cookies to deal with too. Alright, let's move on to the next section, which is the PKCE stuff.
The PKCE Problem
Let's finally dive into this PKCE thing. Once again, this stands for Proof Key Code Exchange. It's basically a security extension that enables the authorization server to verify whether the client who is swapping/exchanging the code is the same as the one who actually requested it. Now without complicating it further, I will explain it practically.
Let's look at how to create a PKCE and code challenge. The below script does exactly what is just mentioned.
The "PKCE" is a python module; you can easily download it via the python-pip package manager. Now the code_verifier is a randomly generated cryptographically secure value. If you read the RFC-7636 OAuth spec, you will find that it mentions that the code verifier is generated via a "suitable random number generator to create a 32-octet sequence ". If you look at the implementation of python's pkce library, you will see they are using "secrets.token_urlsafe" to generate a secure random string. Either way, it's the same thing. Once we have that value set, we have to generate a code challenge, simply an SHA-256 value of the code verifier, which is then base64 URL encoded. The following is the output of the above-shown script.
Once this output is generated, the value of the code challenge is then sent in the request, as you might have seen in the above section. It looks something like this.
code_challange={value_here}&code_challenge_method=S256
In the final stage of the OAuth flow, where a code is received, it needs to be exchanged with an actual access token. This information (the method and the code_challenge) is sent to the endpoint along with an extra value, that is the code_verifier. Here the server records the values, and since the method is S256, the server then takes the code_verifier value and hashes it using the SHA-256 algorithm and base64 URL-encodes the value.
The process (taken from the spec) looks something like this.
BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))
After this step, the server compares this calculated value (the above one) with the code challenge sent with the request in the previous steps. If the value matches and assuming nothing went wrong in the entire process, the server finally hands off the access token to the client.
Overall the process looks something like this.
BASE64URL-ENCODE(SHA256(ASCII(code_verifier))) == code_challenge
That was the entire process in a nutshell. I know this is more complicated, but you will understand it better once you see it in action. You might wonder why this thing is even used. Well, this provides a lot of security in mitigating things like CSRF and authorization injection attacks. In injection attacks, the attacker attempts to inject a stolen authorization code into the attacker's session with the client.
Well, that's it. I won't complicate this any further because we don't need to understand this in-depth to conduct this attack. Alright, with that sorted, let's move on to the next section.
Tracing the OAuth Flow Manually
Finally!! Let's get into the fun stuff now. We will manually trace the entire flow one by one in Burp Suite and try to derive an access token without using the application help at all. Alright, let's dive in!
First, we need to make the first request to the OAuth server. This request contains the user's OTP, phone number, etc. Let's see how it looks after we send that information via Burp.
We have the first request and response to it too. Ignore the response for now. I'll explain that in a minute, but first, let's look at the request. So we set the phone number as something (Of course, I won't display the number!), and the OTP is set to "4005". I generated a brand new code_challange and code_verifier using the script I showed you before and substituted the value of code_challenge here.
Now the response is a bit weird. It sends back a 302 response, technically a redirect, but the Location header looks weird. You might ask, what is this login_challenge thing? Well, let me explain.
The login_challenge is an identifier that the OAuth server uses to fetch important information about the request. Behind the scenes, this is used to communicate to the app servers for verifying information. That's all this thing is. Nothing complicated.
Once we have the first request done, let's make the second request to get something known as a consent_challenge.
If you notice, nothing much has changed except we have an extra parameter named "login_verifier" in the request. This is the parameter where we have to substitute the value of the "login_challenge" we acquired earlier in the first request.
Once the request goes through, we get a 302 status code again, but this time we have something known as a "consent_challenge." The consent challenge stage is where you see this message on your screen saying, "Hey, this app needs access to your google drive and can read, write and modify files on your drive. Do you want to allow this access?" You can see this image in the OAuth Working section of this blog.
You might notice one more thing in this entire process: the two cookies named "oauth2_authentication_csrf" and "oauth2_consent_csrf" in the response. Looking at the names themselves, I am sure you know what these are used for. These are CSRF (Cross-Site Request Forgery) tokens presented as cookies. We need to substitute these two cookies in the subsequent request too, in order to execute the OAuth flow properly.
Once this is done, we have two more steps to complete the entire flow and get the access token. Let's look at the third request we need to make after acquiring the consent challenge value.
Here we can finally see the code! This is a good sign. It means we are close to our final process. If you pay attention to the request tab, we only changed login_verifier to consent_verifier and substituted the value of it to the previously acquired value of consent challenge. Sending the request, we got something different this time.
We get something like com.app.auth://oauth2_callback?code={something}. Ok, if you have some background in android development or android pentesting in general, you can easily deduce that this is an android deep link.
Basically, this is calling an exposed intent or a service registered in the android native code, and that service does further processing of the request. Well, since we aren't operating via the application, we won't need this, but the code part of the response is what interests us. This is the authorization code. This is what we need to exchange to the token endpoint to actually get an access token. So the next request will be the final request to acquire the access token.
Before we move to the final request, I want to mention that the values of both cookies also need to be substituted before making the above request. Anyways let's move on to the final request!
This should be the final nail in the coffin to complete this entire process. Before sending the request, I want to show how this request differs from others. If you look at the route, which is "/oauth2/token" it is different from the others we worked with. If you look at the parameters of the request, you will find that there are far less parameters than the previous ones.
But the most important ones in this request are the code_verifier and code parameters. The code_verifier is the one we created in the PKCE script. The code is the one we got in response to the previous step. This is a crucial step.
In the PKCE section of this blog, I mentioned how the OAuth server takes the code_verifier value, hashes the value with SHA256, and then matches it with the code_challenge value sent in the previous requests.
Anyways, if we send this request and if everything goes well, we should be getting a response with the access token. Alright, let's send the request and see if we get it. Finger Crossed 🤞🤞😧
And yes, we finally got the token. YAYYYYYY!!!!! 🤩🤩🤩🎉. That's it. We got the response containing the entire access token and a refresh token. We were successfully able to complete the entire OAuth flow.
*literally me
Woo!! After this long ride, we finally understand this application's entire OAuth process. Since we know the application does not have a rate limiting in the OTP endpoint; we can fully automate this process and exploit this loophole. Alright then, let's move on to the final section of this blog, exploitation.
The Exploitation
We are finally going to write an exploit to automate this attack fully. I will write this entire thing in python, but you can use whichever language you prefer as long as you can get the PKCE correctly. Alternatively, you can also use the Burp Macros and then use Burp Intruder to get this done, but I wanted to go differently because I wanted a Burp dependency. Anyways let's dive into this.
I will show the exploit in 5-6 parts as images and explain as much as possible along the way because the code is long. You don't need to be a python wizard to understand this exploit.
Let's look at the first part of the exploit. Here there is nothing much but declarations of the URLs we will use for the OAuth flow and stuff. The USER_INFO is the one I'll be using for testing with the access_token to dump the user information. The function init_brute_force_process will run each time for each four-digit combination of OTP in the list. The req_otp function, as the name suggests, requests/sends an OTP to the user's phone.
This sends the first request, which is the login challenge request, to the OAuth server. Most of the parameters passed are the same in the Burp Suite version. The cookie value has been kept static since I never needed to change it, and it still worked despite that.
Now here's the critical part, which is extracting the value of the login_verifier. Now since we know that this request has a status code of 302 which is a redirect, we can be assured that the "Location" header is present too. The Location header tells the browser that, in the case of the 302 status code, it simply redirects to the value of the Location header. So, in this case, we take that header, split the string at the "=" sign, and assign the value of the second element in the array.
That's it. That's the magic. That's all we need to do for the subsequent requests too. Also, remember that the cookie value has been fetched and sent to the next function.
This is the consent challenge stage. It's the same as the login challenge stage, except the login_verifier value is substituted with what was received in the previous stage. We use the same methodology to pull the login_challenge code from the request and also the same for the cookie.
The only exception here is the addition of an error check. This is used to figure out if the "Location" header contains any keyword name error, which at this point, we can restart with a new OTP value since the provided one was wrong.
This is the consent verifier step. We substitute the values of the consent verifier and the two cookies we acquired from the previous request. We access the consent_verifier the same way too.
This is the last stage, acquiring the access token from the authorization code we got from the previous step. This one is straightforward. We take the code verifier value generated in the first step and send the request along with the authorization code.
The result will be an access token. This step is optional, but to verify it properly, I used an endpoint in the application that displays user information using that access token.
Finally, this is just the prep work for starting the entire script. A file named seq.txt is read, which contains a list of all possible four-digit OTP combinations and then iterates over them one by one and executes the init_brute_force_method, which starts the entire brute force process.
That's the entire exploit. Let's run this and see if everything works as intended.
And, of course, it does!!. You can see the script moving through each OTP and cracking the correct one easily.
Conclusion
Well, that was it for the OAuth rate limiting exploitation. If you have made it this far reading this article, I appreciate the persistence and patience you have to make it to the end. My articles are primarily long, and I really have a bad reputation at my workplace for publishing huge articles 😂. But I like to elaborate on things and explain them well.
Well, I hope this blog helped you learn something new and interesting. So next time you encounter an OAuth server and a rate-limiting problem, you know how to exploit it.
References and Materials
https://datatracker.ietf.org/doc/html/rfc7636#page-17
https://github.com/RomeoDespres/pkce
https://datatracker.ietf.org/doc/html/rfc6749
https://dropbox.tech/developers/pkce--what-and-why-
Vineet Nair
When he's not found experimenting with various Linux distributions, he enjoys mountaineering, playing heavy metal on strings, and performing mixed martial arts.
Subscribe now for growth-boosting insights from Appknox
We have so many ideas for new features that can help your mobile app security even more efficiently. We promise you that we wont mail bomb you, just once in a month.