Skip to content

Commit 183749a

Browse files
committed
Added alternative MD4 implementation when OpenSSL lacks it.
Apperently hashlib does not support MD4 with the default configuration of recent OpenSSL versions. Therefore I added a fallback to a pure-Python MD4 implementation by James Seo.
1 parent 79b4c53 commit 183749a

File tree

3 files changed

+190
-3
lines changed

3 files changed

+190
-3
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ How to run
77
----------
88

99
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).
10+
dependencies and the Trustroast scripts solely depends on [Impacket](https://github.com/fortra/impacket).
1111

1212
Run each script with `-h` for usage instructions.
1313

@@ -33,4 +33,4 @@ Trustroasting
3333

3434
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:
3535

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.
36+
- `kirbi_to_hashcat.py`: converts a Kerberos ticket (referal/trust, service, ticket-granting, etc.) that is encoded as a base64 KRB_CRED structure into a Hashcat format. Hash types 13100, 19600, 19700 (i.e. RC-4 and AES tickets) are supported.

timeroast/md4.py

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
#
4+
# Copyright © 2019 James Seo <james@equiv.tech> (github.com/kangtastic).
5+
#
6+
# This file is released under the WTFPL, version 2 (wtfpl.net).
7+
#
8+
# md4.py: An implementation of the MD4 hash algorithm in pure Python 3.
9+
#
10+
# Description: Zounds! Yet another rendition of pseudocode from RFC1320!
11+
# Bonus points for the algorithm literally being from 1992.
12+
#
13+
# Usage: Why would anybody use this? This is self-rolled crypto, and
14+
# self-rolled *obsolete* crypto at that. DO NOT USE if you need
15+
# something "performant" or "secure". :P
16+
#
17+
# Anyway, from the command line:
18+
#
19+
# $ ./md4.py [messages]
20+
#
21+
# where [messages] are some strings to be hashed.
22+
#
23+
# In Python, use similarly to hashlib (not that it even has MD4):
24+
#
25+
# from .md4 import MD4
26+
#
27+
# digest = MD4("BEES").hexdigest()
28+
#
29+
# print(digest) # "501af1ef4b68495b5b7e37b15b4cda68"
30+
#
31+
#
32+
# Sample console output:
33+
#
34+
# Testing the MD4 class.
35+
#
36+
# Message: b''
37+
# Expected: 31d6cfe0d16ae931b73c59d7e0c089c0
38+
# Actual: 31d6cfe0d16ae931b73c59d7e0c089c0
39+
#
40+
# Message: b'The quick brown fox jumps over the lazy dog'
41+
# Expected: 1bee69a46ba811185c194762abaeae90
42+
# Actual: 1bee69a46ba811185c194762abaeae90
43+
#
44+
# Message: b'BEES'
45+
# Expected: 501af1ef4b68495b5b7e37b15b4cda68
46+
# Actual: 501af1ef4b68495b5b7e37b15b4cda68
47+
#
48+
import struct
49+
50+
51+
class MD4:
52+
"""An implementation of the MD4 hash algorithm."""
53+
54+
width = 32
55+
mask = 0xFFFFFFFF
56+
57+
# Unlike, say, SHA-1, MD4 uses little-endian. Fascinating!
58+
h = [0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476]
59+
60+
def __init__(self, msg=None):
61+
""":param ByteString msg: The message to be hashed."""
62+
if msg is None:
63+
msg = b""
64+
65+
self.msg = msg
66+
67+
# Pre-processing: Total length is a multiple of 512 bits.
68+
ml = len(msg) * 8
69+
msg += b"\x80"
70+
msg += b"\x00" * (-(len(msg) + 8) % 64)
71+
msg += struct.pack("<Q", ml)
72+
73+
# Process the message in successive 512-bit chunks.
74+
self._process([msg[i : i + 64] for i in range(0, len(msg), 64)])
75+
76+
def __repr__(self):
77+
if self.msg:
78+
return f"{self.__class__.__name__}({self.msg:s})"
79+
return f"{self.__class__.__name__}()"
80+
81+
def __str__(self):
82+
return self.hexdigest()
83+
84+
def __eq__(self, other):
85+
return self.h == other.h
86+
87+
def bytes(self):
88+
""":return: The final hash value as a `bytes` object."""
89+
return struct.pack("<4L", *self.h)
90+
91+
def hexbytes(self):
92+
""":return: The final hash value as hexbytes."""
93+
return self.hexdigest().encode
94+
95+
def hexdigest(self):
96+
""":return: The final hash value as a hexstring."""
97+
return "".join(f"{value:02x}" for value in self.bytes())
98+
99+
def _process(self, chunks):
100+
for chunk in chunks:
101+
X, h = list(struct.unpack("<16I", chunk)), self.h.copy()
102+
103+
# Round 1.
104+
Xi = [3, 7, 11, 19]
105+
for n in range(16):
106+
i, j, k, l = map(lambda x: x % 4, range(-n, -n + 4))
107+
K, S = n, Xi[n % 4]
108+
hn = h[i] + MD4.F(h[j], h[k], h[l]) + X[K]
109+
h[i] = MD4.lrot(hn & MD4.mask, S)
110+
111+
# Round 2.
112+
Xi = [3, 5, 9, 13]
113+
for n in range(16):
114+
i, j, k, l = map(lambda x: x % 4, range(-n, -n + 4))
115+
K, S = n % 4 * 4 + n // 4, Xi[n % 4]
116+
hn = h[i] + MD4.G(h[j], h[k], h[l]) + X[K] + 0x5A827999
117+
h[i] = MD4.lrot(hn & MD4.mask, S)
118+
119+
# Round 3.
120+
Xi = [3, 9, 11, 15]
121+
Ki = [0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15]
122+
for n in range(16):
123+
i, j, k, l = map(lambda x: x % 4, range(-n, -n + 4))
124+
K, S = Ki[n], Xi[n % 4]
125+
hn = h[i] + MD4.H(h[j], h[k], h[l]) + X[K] + 0x6ED9EBA1
126+
h[i] = MD4.lrot(hn & MD4.mask, S)
127+
128+
self.h = [((v + n) & MD4.mask) for v, n in zip(self.h, h)]
129+
130+
@staticmethod
131+
def F(x, y, z):
132+
return (x & y) | (~x & z)
133+
134+
@staticmethod
135+
def G(x, y, z):
136+
return (x & y) | (x & z) | (y & z)
137+
138+
@staticmethod
139+
def H(x, y, z):
140+
return x ^ y ^ z
141+
142+
@staticmethod
143+
def lrot(value, n):
144+
lbits, rbits = (value << n) & MD4.mask, value >> (MD4.width - n)
145+
return lbits | rbits
146+
147+
148+
def main():
149+
# Import is intentionally delayed.
150+
import sys
151+
152+
if len(sys.argv) > 1:
153+
messages = [msg.encode() for msg in sys.argv[1:]]
154+
for message in messages:
155+
print(MD4(message).hexdigest())
156+
else:
157+
messages = [b"", b"The quick brown fox jumps over the lazy dog", b"BEES"]
158+
known_hashes = [
159+
"31d6cfe0d16ae931b73c59d7e0c089c0",
160+
"1bee69a46ba811185c194762abaeae90",
161+
"501af1ef4b68495b5b7e37b15b4cda68",
162+
]
163+
164+
print("Testing the MD4 class.")
165+
print()
166+
167+
for message, expected in zip(messages, known_hashes):
168+
print("Message: ", message)
169+
print("Expected:", expected)
170+
print("Actual: ", MD4(message).hexdigest())
171+
print()
172+
173+
174+
if __name__ == "__main__":
175+
try:
176+
main()
177+
except KeyboardInterrupt:
178+
pass

timeroast/timecrack.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,18 @@
1111
from argparse import ArgumentParser, FileType, RawDescriptionHelpFormatter
1212
import hashlib, sys
1313

14+
def md4(data):
15+
try:
16+
return hashlib.new('md4', data).digest()
17+
except ValueError:
18+
# Use pure-Python implementation by James Seo in case local OpenSSL does not support MD4.
19+
from md4 import MD4
20+
return MD4(data).bytes()
21+
1422
def compute_hash(password, salt):
1523
"""Compute a legacy NTP authenticator 'hash'."""
16-
return hashlib.md5(hashlib.new('md4', password.encode('utf-16le')).digest() + salt).digest()
24+
return hashlib.md5(md4(password.encode('utf-16le')) + salt).digest()
25+
1726

1827
def try_crack(hashfile, dictfile):
1928
# Try each dictionary entry for each hash. dictfile is read iteratively while hashfile is stored in RAM.

0 commit comments

Comments
 (0)