0-click Full Account Takeover

dynnyd20
10 min readAug 19, 2024

--

Hello, my friends.

It has been four months since my last blog post, and my bug hunting activities have also been on hold for many months. In recent weeks, due to various reasons (specifically temporary unemployment), I have returned to bug hunting and can now spend more time on it.
This time, I decided to choose a few programs with many functionalities and test them over 3 weeks. During this period, I discovered and reported several vulnerabilities related to broken access control, business logic, and a few other minor issues that I have not reported yet. After a few weeks with these programs, I decided to move on to another one. However, before switching to a different program, I chose to spend my time delving deeply into the “forgot password” function of a program and attempting to exploit it. This is where “0-click Full Account Takeover” happened…

Let's get started!

During testing forgot password function, I went through numerous attempts with various ideas and encountered many failures. I will detail all of them in this blog.

Forgot Password Flow:

The forgot password function flow has 4 APIs as below, I will not explain the details of this flow here, but I will explain it later in this blog post:

  • “POST /cia/***/recovery/getoptions” to get the reset password options
  • “POST /cia/***/recovery/send” to request the OTP
  • “POST /cia/***/recovery/verify/email” to verify the OTP
  • “POST /cia/***/recovery/reset” to change the password.

First attempt:

Sure, first I tried to brute-force the OTP sent to the victim’s email. This is the simplest approach that anyone would think of first. I know that it seems like no hope as this is a program that has been running for 4 years and has been researched by many other ethical hackers before.

However, I will still give it a try, because my view is that anything can happen. I have read many other blog posts, and several applications have this issue. So who knows…

And… Do you think it worked?

No! It fails.

The application will terminate the OTP after 10 times failure on validation.

=> My first attempt fails

Second attempts:

Next, I tried another simple way usually used during pentesting the forgot password flow.

I tried skipping the OTP confirmation step and forcing a call to the password reset API. And the result was just as I thought, it failed.

=> My second attempt failed

But that’s okay. To me, this is a reasonable result. I began to develop different ideas to bypass the application’s limitations and brute-force the OTP.

While requesting an OTP to reset the password, one aspect of the application caught my attention: within 30 minutes, no matter how many times you request an OTP for a password change, only one OTP value is generated (all the OTP has the same value). I felt there might be something exploitable based on this, so I decided to return to the application and investigate the “forgot password” flow more deeply.

So before moving on to other attempts, let's take a moment to pause and better understand this flow together!

Deep into forgot password flow:

  1. Get reset password options API:

When this request is sent to the server, if the “IDHUB_JSESSIONID” cookie is empty or has expired, the server will generate a new “IDHUB_JSESSIONID” cookie. This cookie will represent the user requesting the password reset and will be used throughout the password reset process.

On the other hand, if the “IDHUB_JSESSIONID” cookie is valid and not expired, no new “IDHUB_JSESSIONID” cookie will be generated.

All the next steps will use the “IDHUB_JSESSIONID” above to validate and perform the function on the user.

2. Request the OTP code:

With the options returned in the get reset password options API, request the OTP code. The OTP will be sent to the user’s phone number or email depending on the choice.

3. Validate the OTP code:

4. Set the new password:

Alright, I think it’s time for me to return to my attempts.

As I described in my first attempt, the OTP will terminate after 10 times failure validation. However, one thing that I must pay attention to is that the OTP will not be changed in 30 minutes no matter how many times I request the OTP (1).

What I’m most unclear about right now is what the application uses to terminate the OTP. Based on my limited experience, I would guess it could be the number of failed OTP validations per an OTP request (2), for example.

Combining (1) and (2), the idea for my third attempt emerged.

Third attempt:

My idea is to write a script to automatically brute-force the OTP and request a new OTP after every 9 attempts.

However, my idea failed when I tested it for two reasons:

  • After 9 incorrect OTP entries, I tried to request a new OTP and continued. To verify whether my idea worked, I tried to enter the incorrect OTP one time and enter correct OTP afterthat, but unfortunately, I received the response “INVALID_OTP”
  • There is a limit on the number of OTP requests that can be made. Therefore, even if this idea worked, we wouldn’t be able to exploit it due to this limit.

=> My third attempt failed

However, it's too soon to give up. I started building a new idea.

Fourth Attempt:

I began to think further about what conditions might lead the application to terminate the OTP. At this point, I considered the session. I thought of trying to leverage the fact that a new “IDHUB_JSESSIONID” cookie will be generated when I call the API to get reset password options with an empty cookie. This idea led to my fourth attempt.

This idea only works if the newly created “IDHUB_JSESSIONID” but not used to request OTP can be used to confirm OTP. So to confirm it, I tested as below:

get reset password options -> request OTP -> get reset password options with empty cookie to create new “IDHUB_JSESSIONID” cookie -> confirm correct OTP with “IDHUB_JSESSIONID” cookie => success

This gave me hope because the first requirement matched.

I move to the next step as described in the above image.

get reset password options -> request OTP -> verify the OTP fail 9 times -> get reset password options with empty cookie to create new “IDHUB_JSESSIONID” cookie => comfirm with wrong OTP => confirm with correct OTP => FAIL

Yup, It failed!

=> My fourth attempt failed

At this point, I have drawn my conclusion. The mechanism for generating and terminating OTP in the application is based on the user. Specifically, within a 30-minute period, a user is only generated only one OTP on the server. This OTP is associated with the user and the user can only be entered incorrectly a maximum of 9 times. If a user enters the OTP incorrectly more than 9 times, the current OTP on the system will become invalid. However, it will still exist on the system for a period of 30 minutes, and the user must wait until this time elapses before they can request the server to generate a new valid OTP.

I stop my idea to brute-force OTP here.

The next day, I took a different approach…

Take a look back at the reset password flow, there are 2 things for me to consider:

  • OTP verification API and the password reset API are two separate APIs. Skipping the OTP verification step and proceeding with the password reset fails (as seen in the second attempt). Additionally, in the password reset request, only the “password” parameter is required, so the server will certainly check whether this session has successfully verified the OTP before resetting password for the user.
  • A new “IDHUB_JSESSIONID” cookie will not be generated if we use a valid cookie to get request password options. Using this logic, I can make a valid session that is currently pointing to user A switch to point to user B.

=> So, at that point, I wonder if I can create a session that is successfully OTP verified by the attacker but points to the victim. Maybe, so let’s try.

Fifth Attempt:

As described above, my new idea:

  • Step 1: As the attacker, request OTP and confirm OTP successfully and stop
  • Step 2: Save the session that has been confirmed successfully by the attacker
  • Step 3: With the saved session, send the request the get reset password options for the victim to point this session to the victim’s account
  • Step 4: With the saved session, force calling reset password API to reset to victim’s password

This idea sounds promising, so I tested it immediately afterward. And the result…

=> Fifth attempt: Another fail…Hehe.
One thing I am very clear about is that if I continue, I might either succeed in exploiting it or not; there could be bug or there might not be. Truly, no one can predict what might happen next. And I decided to continue just because I wanted to. I want to fully exploit it with all the effort I can muster.

At this point, I began to review my logic again. If I assume that my idea is correct but the results are failing, then it’s clear that I must have missed something in the process of implementing my idea. And can you guess it…what I missed...?

Sixth Attempt and The FINAL:

I reviewed my logic and the forgot password flow again. Let’s see: as the victim, only the get reset password options API and reset password API were called. The request OTP API was not called for this user. This means that no valid OTP for this user was generated. So the flow to reset the password will fail if the server checks whether the current user has generated a valid OTP.

=> I decided to add a step to request OTP for the victim into the flow in fifth attempt and perform my sixth attempt:

  • Step 1: As the attacker, request OTP and confirm OTP successfully and stop
  • Step 2: Save the session that has been confirmed successfully by the attacker
  • Step 3: With the saved session, send the request the get reset password options for the victim to point this session to the victim’s account
  • Step 4: With the saved session, send the OTP request for the victim
  • Step 5: With the saved session, force calling reset password API to reset to victim’s password

Finally, it worked.

After the password reset request was sent to the server and responded with a successful result, I knew that my sixth attempt had reached 80% of success. I immediately opened the browser and logged into the victim’s account with the new password.

And boom! I successfully log into victim’s account.

And there was no doubt, this attempt was a complete success. By exploiting this vulnerability, I could change the password of any account.

=> Completely takeover any account.

In Conclusion

The root cause of this vulnerability is that the application only checks whether the session is successfully OTP verified or not, but does not check whether the user verified OTP successfully or not. As a result, an attacker could verify the OTP with the OTP sent to his mail, and then use the session that verified the OTP successfully before to change the victim’s password.

I reported this finding and some hours later, I got the response from the Intigriti triager and the Company. The issue was also fixed on that day.

After discussion with the Company, We agree that this finding has a High impact with a bonus from the Company because of the Privilege Required (This application has no function for users to register accounts by themselves, so to exploit this vulnerability, an attacker must have an account with low privilege before).

I’d be lying if I said I didn’t have a little regret about this one, I was hoping for a higher impact. However, I was pleased with the appreciation from the company for my work on this finding. Yes, the bounty is indeed meaningful to me, but it is even more significant knowing that I was able to help them.

When I first started doing pentesting, I was taught to try my best to find and report all bugs, no matter how low impact or high impact. My boss probably didn’t know that I used to wonder if such low impact bugs would have any impact on the user system, whether it was necessary to report them (although I still reported them because I was afraid of being exposed by my colleagues). Later, when I did more pentesting projects and take more responsibility in the projects, I felt that it was really worth it, a small weakness can be an important part of a big attack for talented hackers, it is a low-impact bug to me but can completely become a part of a high impact bug when it is in the hands of others.
I’m not a talent hacker, for real. All I can do is love and respect what I do first and keep going ahead.

Happy hacking! Thanks for reading.

--

--

dynnyd20
dynnyd20

Responses (5)