This post briefly examines two flawed implementations of Multi-Factor Authentication (MFA) in web applications. MFA is great and I completely support it, but in each of the applications the second factor was evaluated independently of the first factor, which allowed mixing a username/password of account ‘A’ with second factor of account ‘B’. In one application this led to a bypass of the second factor. In the other application it led to a privilege escalation.
Application One: Second factor bypass and no account lockout
This system allowed self-registration, and used Okta to enforce authentication. If you don’t know what Okta is, it is an identity and access management solution that can integrate into your applications to provide centralized management and a single-sign-on like experience. Before I go further, I want to mention that I could not reproduce this on <domain>.okta.com, so I do not think there is anything wrong with Okta, but rather with the implementation on the specific application I was testing.
Let’s step through the normal authentication process into this system, and then discuss the flaws.
Login with a username and password:
The server checks if the username/password is correct, and responds with data necessary to submit your second factor, specifically a state token, your oktaUserId, and your factorId:
After the above response, a request is automatically submitted to initiate the SMS notification for the second factor, which you are then prompted for:
Once the second factor is submitted and verified, an additional authentication request is automatically made.
The server evaluates the credentials and responds with a JWT if valid, which allows you access to the application.
No account lockout is great (for me), but even if we can guess the password we still have to worry about the second factor. Luckily, the application validated the MFA and authentication requests separately. Here is what I did:
Without account lockout I was able to guess another user’s password. I already had a valid account on the system (since I could self-register), so I performed the initial username/password login (which returned my stateToken and factorId), submitted my SMS MFA code, and then intercepted the second username/password authentication request, swapping out the username and password with the one I was able to guess (due to no account lockout):
Which then gave me a JWT that allowed me access to the application as that user:
Application Two: Privilege escalation to any user
This flaw was more serious. The application did not have self-registration, but my team and I were each given two accounts (one user, one admin) to test with. This system used Symantec VIP for the second factor. Once again, there is nothing wrong with using Symantec VIP, but in this case the implementation was flawed. Let’s step through the normal authentication process into this system, and then discuss the flaws.
Login with a username and password:
The server checks the username and password, returns data necessary to submit and validate the second factor, specifically a hidUserId and an hidMfaId, which were nothing more than a tamperproof querystring value, which is essentially a parameter with a signature, encoded in base64. The value of a tamperproof querystring looks like this:
The left side of the ‘-‘ decodes to 2778 and the right side of the ‘-‘ is the signature, but even if I wanted to change that value and re-encode it, the signature wouldn’t be correct and I’d get an error. The hidUserId is just a number, and the hidMfaId is just the username, but both are encoded with the tamperproof querystring.
The hidUserId and hidMdaId values are populated in the response, and I am prompted to submit my second factor:
The security code gets submitted, along with the hidUserId and the hidMfaId, and if the code checks out then access is granted:
Similar to the first system, this application handled the MFA as a two-step process. The first request evaluated the username/password and returned the tamperproof hidUserId and the hidMfaId, and the second request evaluated the security code. The issue I noticed with this app, is that when the security code is submitted, the hidUserId also gets submitted. I started wondering if I could substitute another valid hidUserId and impersonate that user. The problem of course, was that the hidUserId was a tamperproof string, and I had no way of getting or guessing the value.
Luckily the application had another bug, and I was able to get that value. Here is what I did:
If I submitted a login with a valid username and invalid password, the application wouldn’t give me the hidUserId, however, the state would change, and when I attempted a login again (with the same valid username and invalid password), the hidUserId would be dynamically generated and would be submitted in the request. With this bug, I was able to get the hidUserId of any user, so I chose an admin account:
Now I tested to see if I could swap the hidUserId of the admin user in a valid authentication request of a regular user. So once again, I used my regular user account, pentest_jake, and logged in with a username and password:
Then I submitted my security code and intercepted the request, swapping out my hidUserId value with the admin account UserId:
This granted me access to the application as the admin user:
I definitely want to reiterate that I fully support MFA, but implementation can sometimes be tricky, and these bugs are probably not uncommon.