Solving PortSwigger’s ‘2FA bypass using a brute-force attack’ Lab with OWASP ZAP

cerulean
12 min readAug 22, 2022

--

Introduction

PortSwigger provides some excellent labs to practice various aspects of penetration testing and bug hunting. This article will outline how I’ve managed to solve the ‘2FA bypass using a brute-force attack’ expert level lab using OWASP ZAP.

Disclaimer: I’m not an expert bug hunter by any means and this is just the process that I went through to find the solution. Feedback and comments are welcome.

Warning: spoilers ahead. If you haven’t completed the Lab, I recommend you try it out before reading the solution.

The lab description outlines the goal of the lab which is to use the provided credentials (carlos:montoya) and bypass the 2FA mechanism to log into the account.

Exploration

The first thing I do when starting a lab is explore the application manually by entering the URL into the ‘Manual Explore’ box of ZAP under the Quick Start tab and launching the browser. I then look at the sites tree in ZAP which will now be populate with various domains. As I’m only interested in the URL relating to the lab, I right click on the domain and add it to the default context.

You can now toggle the ‘Show all URLs’ option in the Sites tab and the History tab to show only the URLs in scope.

At this point, I went back to the browser and explored my application manually by attempting to log in on the ‘My Account’ page with the credentials provided in the description. Notice that after submitting the credentials, you are taken to a second step in the authentication process for which the URL ends with /login2.

Make a few attempts to submit arbitrary 4-digit codes and notice that after the first unsuccessful attempt, you are given a second attempt to enter a code, but after the second unsuccessful attempt you are taken back to the first step of the authentication process.

Exploitation

Naturally, the next thing I tried was to Fuzz the mfa tokens on the /login2 URL. On the History tab of ZAP, find the POST request for the /login2 URL and you’ll notice the parameter ‘mfa-code’ with your previously submitted value in the body of the request, as well as a ‘csrf’ parameter with an arbitrary string (which we’ll ignore for now). Highlight the value of the mfa-code, right click and select ‘Fuzz’

The Fuzz function allows you to replace any item of the request with a list of payloads and send a new request for each payload on the list. In our case we will try to brute force the mfa-code by submitting a payload with every possibility of the 4 digit token. In the Fuzzer window, click ‘Payloads’ and click ‘Add’ to add a new payload. Select the type ‘Numberzz’ and set the ‘From’ value to 0 and the ‘To’ value to 9999. Click ‘Generate Preview’ to see a preview of what the payloads will look like.

Notice that the payload won’t quite work just yet as the numbers don’t contain 4 digits. To turn them into 4 digit numbers we need to add a payload processor. Click ‘Add’ on the ‘Add a payload’ window. Select the Numberzz payload we just added and click ‘Processors’. Click ‘Add’ to add a new processor and set the Type to ‘Expand’. Enter ‘0’ in the value field and click ‘Generate Preview’. Notice that we now have 4 digits for every payload.

Click ‘Add’ and ‘OK’ everything else and then click ‘Start Fuzzer’ to start the process.

Your ZAP window will now focus on the ‘Fuzzer’ tab on the bottom panel with details of each request including the payload and the server response. Taking a glance at the ‘Code’ and ‘Reason’ column it quickly becomes obvious that this attack didn’t work as we’re getting loads of Code 400 responses with reason ‘Bad Request’. A code 400 response basically means that the server will not process the request due to a client error.

Press stop on the Fuzzer to cancel the process. Let’s analyse why it’s failing. Click on one of the Code 400 rows and then click on ‘Response’ within the top-right panel. Notice the message in the body of the response states “Invalid CSRF token (session does not contain a CSRF token)”.

If you’re unfamiliar with CSRF check out OWASP’s write up or PortSwigger’s Academy article on the topic.

It seems like we’ll need a new (and valid) CSRF token sent with every single (or every second) POST request with the mfa-code. So how do we generate CSRF tokens? Let’s analyse some of our previous requests in more detail. Head back to ZAP’s History tab, click on the GET request for the /login page and click on the Response tab to analyse the login page. Scroll to the bottom of the response and notice that there is a hidden ‘csrf’ form which provides a random value.

Now click on the POST request for the /login page following the GET request, and look at the request. Notice that the csrf value that our browser submitted in the POST request matches the value we received in the response of the GET request.

Analyse the requests for the /login2 page and you’ll notice the same thing -when you are redirected from /login to /login2, there is another hidden form which generates a new csrf token for the second step of the login.

So the two login pages generate valid csrf tokens which you need to submit along with each POST request (login attempt) for the attempt to be valid.

My initial idea was to try to generate a large number of csrf tokens which I could then use along with each 4 digit code payload. By right clicking on the GET request for the /login page and clicking ‘Open/Resend with request editor’ we can request this page multiple times and analyse the response.

In the ‘Manual Request Editor’ window simply click Send and look for the csrf value at the bottom of the response. We have what seems like a new CSRF token.

However, if we click Send again and look at the csrf token again, you’ll notice that it doesn’t change, so this doesn’t seem to work in generating multiple tokens.

I then looked to the ZAP marketplace to see if there was any useful add-on which can help us generate csrf tokens and found an add-on called ‘Token Generation and Analysis’ for which the description states ‘Allows you to generate and analyze pseudo random tokens, such as those used for session handling or CSRF protection’. Sounds relevant, let’s give it a go. Click on the Manage Add-ons icon within ZAP, go to the Marketplace tab, type in ‘csrf’ or ‘token’, tick the ‘Token Generation and Analysis’ add-on and click ‘Install Selected’.

After installing the add-on, we now have an extra option ‘Generate Tokens’ when right clicking on a response. Firstly, let’s select a GET request for the /login page from the history tab, click on the Response tab and right click on the response window. Then select ‘Generate Tokens’

In the Generate Tokens window, set the Type to ‘form’ and the name should already be set to ‘csrf’ as it already picked this up the from the request. Set the number of tokens to 5 initially to perform a small test and click Generate.

You will now get a new ‘Analyse Tokens’ window with an analysis of the quality of the tokens being generated by the server. We can ignore this window as we’re only interested in the tokens themselves.

Close the ‘Analyse Tokens’ window and notice that there’s now a new tab ‘Token Gen’ on the bottom panel which contains our 5 generated tokens. Click on the ‘Save Tokens’ button and select a location to save the generated tokens.

Open the file with a text editor and copy one of the tokens. Go back to the History tab in ZAP and find the POST request for the /login page. Right click on the request and click ‘Open/Resend with request editor’. Replace the value of the ‘csrf’ parameter with the token that you copied from the tokens that we generated and click Send.

Here’s the response that we get from the server.

Well that didn’t work. The CSRF token wasn’t accepted. So it doesn’t look like these pre-generated csrf tokens will work in our brute-force attempts. It seems we’ll need to perform the full authentication process from beginning to end with every brute-force attempt.

Actual Exploitation

The full process should look something like this:

  1. Get a CSRF token on the /login page.
  2. POST the CSRF token along with the credentials on the /login page.
  3. GET another token (CSRF2) on the /login2 page.
  4. POST the CSRF2 token along with a 4 digit code (e.g. 0001) on the /login2 page.

After step 4 we repeat the process from step 1 until the correct mfa-code is found. So how can we automate this process? One way to achieve this is to create a Zest script.

We can either record a Zest script automatically and modify it to suit our needs or we can manually add the required steps to a new Zest script which gives us finer control over each step. I will show the process of manually creating the Zest script.

Firstly, highlight the following requests from the History tab of ZAP (hold ctrl to select multiple items):

  1. GET request on the /login page
  2. POST request on the/login page
  3. GET request on the /login2 page
  4. POST request on the /login2 page

Right click on the highlighted lines, select ‘Add to Zest Script’ and click ‘New Zest Script’.

In the ‘Add Zest Script’ window untick ‘Response Length Assertion’ in the ‘Default Assertions’ tab. Leave everything else as default and go to the ‘Summary’ tab. Give the script a name (i.e. ‘MFA Brute Force’) and click ‘Save’.

You will now see the ‘MFA Brute Force’ script in the Scripts tab on the panel on the left. Expand the script tree to see the requests which we have added along with some extra ‘Ascert’ actions which were added to automatically handle the csrf tokens. Click on the last POST request and click on the Request tab on the panel on the right. Notice that the mfa-code value is hard coded with the value you previously inputted when attempting the login.

We need to change this mfa-code value to a variable and loop the whole script to test each 4 digit combination. Double click on the last POST request in the script and replace the mfa-code value with a variable by using double curly braces (the same as the csrf variable) and click Save. I’ve called mine ‘mfa’.

Now we need to create a loop for the mfa variable. Highlight all the lines in the script, right click on the highlighted lines, select ‘Surround with…’ and click ‘Loop String’.

In the ‘Add Zest Loop’ window, enter the variable name from the previous step and add the 4 digit code payloads to the ‘Values’ section. You can either generate the codes yourself in Excel or copy the payloads from a list and click ‘Save’.

Expand the script again and you will now see the Loop at the top of the script with each request falling inside the loop section, however in my case, for some reason the order of the requests got messed up when I added the loop. I’ve had to re-arrange them back to the correct order (the same as in the previous screenshot) by dragging and dropping each line into its correct place. I also deleted the ‘Assign csrf3’ line as it wasn’t required.

The last step is to add a condition which will stop the script once the correct 4 digit code has been found. We know from previous experimentation that incorrect 4 digit codes result in a code 200 response so we can use this as the condition. Right click on the last POST request in the script, select ‘Add Zest Condition’ and click ‘Status Code’.

In the ‘Add Zest Condition’ window set Status Code to 200 and click ‘Save’.

The condition will appear at the bottom of the script. Right click on ‘ELSE’, select ‘Add Control’ and click ‘Break’.

The script is now ready to run. Click on the ‘Script Console’ tab on the panel on the right and click ‘Run’ to execute the script.

You will now see each request being executed in the ‘Zest Results’ tab on the panel on the bottom.

It may take a while so grab a coffee and let the script run until you see that it has stopped.

Finishing Up

When the correct mfa-code has been found the script will stop. To finish the challenge click on the last POST request in the Zest Results and click on the Response tab on the panel on the right and copy the session cookie.

Go to the History tab on the panel on the bottom and find a request for the /my-account page. Right click on the request and click ‘Open/Resend with request editor’.

In the ‘Manual Request Editor’ window, replace the ‘session’ cookie with the cookie we previously copied from the previous step and click Send.

Scroll down in the response and you will see that you are logged in as the user ‘carlos’.

Congratulations! You have completed the challenge.

I hope you found this write up informative and it has inspired you in your hacking journey. Please share your thoughts in the comments below. Thanks for reading!

Going Above and Beyond

There is one way in which we can make the script more efficient. If you recall our experimentation when we tested the authentication process manually, we were able to test 2 mfa-codes before being kicked back to the first stage of the login process. We can change the script to test 2 mfa-codes for every loop which translates to 2 mfa codes for every 5 requests vs 2 mfa codes for every 8 requests with the original script. This drops the maximum number of requests from 40,000 to 25,000 which is almost a 50% efficiency improvement.

However, the script becomes significantly more complex and to keep this article from becoming even longer, I will just provide a screenshot of the final script which you can probably use to re-construct it for yourself if you so wish.

--

--