Skip to content

Commit abcdca2

Browse files
committed
implement wordlist feature using raw md4
1 parent 8109953 commit abcdca2

File tree

1 file changed

+252
-5
lines changed

1 file changed

+252
-5
lines changed

src/hash_sentinel.cr

Lines changed: 252 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,223 @@
11
require "option_parser"
22

33
file_path = ""
4+
wordlist_path = ""
45

56
OptionParser.parse do |parser|
6-
parser.banner = "Usage: hash-sentinel -f FILE"
7+
parser.banner = "Usage: hash-sentinel -f FILE [-w WORDLIST]"
78

89
parser.on("-f FILE", "--file FILE", "File to read (e.g., enabled_users.txt)") do |file|
910
file_path = file
1011
end
1112

13+
parser.on("-w FILE", "--wordlist FILE", "Optional: Wordlist to identify plaintext passwords (use small lists only!)") do |file|
14+
wordlist_path = file
15+
puts "⚠️ NOTE: This is NOT a password cracking tool and is very inefficient."
16+
puts " Please use only small wordlists or POT files from existing cracking sessions (< 1000 entries)."
17+
end
18+
1219
parser.on("-h", "--help", "Show help message") do
1320
puts parser
1421
exit
1522
end
1623
end
1724

25+
# NT hash function implementation
26+
# NT hash is MD4(UTF-16LE(password))
27+
def calculate_nt_hash(password : String) : String
28+
# Convert to UTF-16LE (little endian)
29+
utf16_bytes = encode_utf16le(password)
30+
# Calculate MD4 hash using our custom function
31+
md4_hash(utf16_bytes).upcase # Return uppercase to match expected format
32+
end
33+
34+
# Corrected UTF-16LE encoding function
35+
def encode_utf16le(str : String) : Bytes
36+
# Allocate buffer directly - 2 bytes per character
37+
bytes = IO::Memory.new
38+
39+
str.each_char do |char|
40+
cp = char.ord.to_u16
41+
bytes.write_bytes(cp, IO::ByteFormat::LittleEndian)
42+
end
43+
44+
bytes.to_slice
45+
end
46+
47+
# Corrected MD4 implementation based on RFC 1320
48+
def md4_hash(input : Bytes) : String
49+
# Initialize state (A, B, C, D) - little endian
50+
a = 0x67452301_u32
51+
b = 0xefcdab89_u32
52+
c = 0x98badcfe_u32
53+
d = 0x10325476_u32
54+
55+
# Get input length in bits and bytes
56+
input_len_bytes = input.size
57+
input_len_bits = input_len_bytes * 8
58+
59+
# Calculate number of padding bytes needed
60+
# Need to pad to 64-byte boundary (512 bits)
61+
# Need at least 9 bytes: 1 for 0x80 and 8 for length
62+
# If less than 9 bytes remaining, need an additional block
63+
padding_bytes = 64 - (input_len_bytes % 64)
64+
padding_bytes = padding_bytes < 9 ? padding_bytes + 64 : padding_bytes
65+
66+
# Create padded message
67+
padded_msg = Bytes.new(input_len_bytes + padding_bytes)
68+
69+
# Copy input data
70+
input_len_bytes.times do |i|
71+
padded_msg[i] = input[i]
72+
end
73+
74+
# Add padding: first byte is 0x80, rest are zeros
75+
padded_msg[input_len_bytes] = 0x80_u8
76+
77+
# Add length as 64-bit integer at the end (little-endian)
78+
# For NT hash, we only need the first 32 bits
79+
padded_msg[padded_msg.size - 8] = (input_len_bits & 0xFF).to_u8
80+
padded_msg[padded_msg.size - 7] = ((input_len_bits >> 8) & 0xFF).to_u8
81+
padded_msg[padded_msg.size - 6] = ((input_len_bits >> 16) & 0xFF).to_u8
82+
padded_msg[padded_msg.size - 5] = ((input_len_bits >> 24) & 0xFF).to_u8
83+
84+
# Process each 64-byte block
85+
0.step(by: 64, to: padded_msg.size - 1) do |i|
86+
break if i + 64 > padded_msg.size
87+
88+
# Break chunk into 16 32-bit words (X array)
89+
x = Array(UInt32).new(16, 0_u32)
90+
16.times do |j|
91+
x[j] = padded_msg[i + 4*j].to_u32 |
92+
(padded_msg[i + 4*j + 1].to_u32 << 8) |
93+
(padded_msg[i + 4*j + 2].to_u32 << 16) |
94+
(padded_msg[i + 4*j + 3].to_u32 << 24)
95+
end
96+
97+
# Save state
98+
aa = a
99+
bb = b
100+
cc = c
101+
dd = d
102+
103+
# Round 1
104+
# [abcd k s]: a = (a + F(b,c,d) + X[k]) <<< s
105+
# F(x,y,z) = (x & y) | ((~x) & z)
106+
107+
# a = round1_op(a, b, c, d, x[0], 3)
108+
a = ((a &+ ((b & c) | ((~b) & d)) &+ x[0]) << 3) | ((a &+ ((b & c) | ((~b) & d)) &+ x[0]) >> 29)
109+
d = ((d &+ ((a & b) | ((~a) & c)) &+ x[1]) << 7) | ((d &+ ((a & b) | ((~a) & c)) &+ x[1]) >> 25)
110+
c = ((c &+ ((d & a) | ((~d) & b)) &+ x[2]) << 11) | ((c &+ ((d & a) | ((~d) & b)) &+ x[2]) >> 21)
111+
b = ((b &+ ((c & d) | ((~c) & a)) &+ x[3]) << 19) | ((b &+ ((c & d) | ((~c) & a)) &+ x[3]) >> 13)
112+
113+
a = ((a &+ ((b & c) | ((~b) & d)) &+ x[4]) << 3) | ((a &+ ((b & c) | ((~b) & d)) &+ x[4]) >> 29)
114+
d = ((d &+ ((a & b) | ((~a) & c)) &+ x[5]) << 7) | ((d &+ ((a & b) | ((~a) & c)) &+ x[5]) >> 25)
115+
c = ((c &+ ((d & a) | ((~d) & b)) &+ x[6]) << 11) | ((c &+ ((d & a) | ((~d) & b)) &+ x[6]) >> 21)
116+
b = ((b &+ ((c & d) | ((~c) & a)) &+ x[7]) << 19) | ((b &+ ((c & d) | ((~c) & a)) &+ x[7]) >> 13)
117+
118+
a = ((a &+ ((b & c) | ((~b) & d)) &+ x[8]) << 3) | ((a &+ ((b & c) | ((~b) & d)) &+ x[8]) >> 29)
119+
d = ((d &+ ((a & b) | ((~a) & c)) &+ x[9]) << 7) | ((d &+ ((a & b) | ((~a) & c)) &+ x[9]) >> 25)
120+
c = ((c &+ ((d & a) | ((~d) & b)) &+ x[10]) << 11) | ((c &+ ((d & a) | ((~d) & b)) &+ x[10]) >> 21)
121+
b = ((b &+ ((c & d) | ((~c) & a)) &+ x[11]) << 19) | ((b &+ ((c & d) | ((~c) & a)) &+ x[11]) >> 13)
122+
123+
a = ((a &+ ((b & c) | ((~b) & d)) &+ x[12]) << 3) | ((a &+ ((b & c) | ((~b) & d)) &+ x[12]) >> 29)
124+
d = ((d &+ ((a & b) | ((~a) & c)) &+ x[13]) << 7) | ((d &+ ((a & b) | ((~a) & c)) &+ x[13]) >> 25)
125+
c = ((c &+ ((d & a) | ((~d) & b)) &+ x[14]) << 11) | ((c &+ ((d & a) | ((~d) & b)) &+ x[14]) >> 21)
126+
b = ((b &+ ((c & d) | ((~c) & a)) &+ x[15]) << 19) | ((b &+ ((c & d) | ((~c) & a)) &+ x[15]) >> 13)
127+
128+
# Round 2
129+
# [abcd k s]: a = (a + G(b,c,d) + X[k] + 0x5a827999) <<< s
130+
# G(x,y,z) = (x & y) | (x & z) | (y & z)
131+
a = ((a &+ ((b & c) | (b & d) | (c & d)) &+ x[0] &+ 0x5a827999_u32) << 3) | ((a &+ ((b & c) | (b & d) | (c & d)) &+ x[0] &+ 0x5a827999_u32) >> 29)
132+
d = ((d &+ ((a & b) | (a & c) | (b & c)) &+ x[4] &+ 0x5a827999_u32) << 5) | ((d &+ ((a & b) | (a & c) | (b & c)) &+ x[4] &+ 0x5a827999_u32) >> 27)
133+
c = ((c &+ ((d & a) | (d & b) | (a & b)) &+ x[8] &+ 0x5a827999_u32) << 9) | ((c &+ ((d & a) | (d & b) | (a & b)) &+ x[8] &+ 0x5a827999_u32) >> 23)
134+
b = ((b &+ ((c & d) | (c & a) | (d & a)) &+ x[12] &+ 0x5a827999_u32) << 13) | ((b &+ ((c & d) | (c & a) | (d & a)) &+ x[12] &+ 0x5a827999_u32) >> 19)
135+
136+
a = ((a &+ ((b & c) | (b & d) | (c & d)) &+ x[1] &+ 0x5a827999_u32) << 3) | ((a &+ ((b & c) | (b & d) | (c & d)) &+ x[1] &+ 0x5a827999_u32) >> 29)
137+
d = ((d &+ ((a & b) | (a & c) | (b & c)) &+ x[5] &+ 0x5a827999_u32) << 5) | ((d &+ ((a & b) | (a & c) | (b & c)) &+ x[5] &+ 0x5a827999_u32) >> 27)
138+
c = ((c &+ ((d & a) | (d & b) | (a & b)) &+ x[9] &+ 0x5a827999_u32) << 9) | ((c &+ ((d & a) | (d & b) | (a & b)) &+ x[9] &+ 0x5a827999_u32) >> 23)
139+
b = ((b &+ ((c & d) | (c & a) | (d & a)) &+ x[13] &+ 0x5a827999_u32) << 13) | ((b &+ ((c & d) | (c & a) | (d & a)) &+ x[13] &+ 0x5a827999_u32) >> 19)
140+
141+
a = ((a &+ ((b & c) | (b & d) | (c & d)) &+ x[2] &+ 0x5a827999_u32) << 3) | ((a &+ ((b & c) | (b & d) | (c & d)) &+ x[2] &+ 0x5a827999_u32) >> 29)
142+
d = ((d &+ ((a & b) | (a & c) | (b & c)) &+ x[6] &+ 0x5a827999_u32) << 5) | ((d &+ ((a & b) | (a & c) | (b & c)) &+ x[6] &+ 0x5a827999_u32) >> 27)
143+
c = ((c &+ ((d & a) | (d & b) | (a & b)) &+ x[10] &+ 0x5a827999_u32) << 9) | ((c &+ ((d & a) | (d & b) | (a & b)) &+ x[10] &+ 0x5a827999_u32) >> 23)
144+
b = ((b &+ ((c & d) | (c & a) | (d & a)) &+ x[14] &+ 0x5a827999_u32) << 13) | ((b &+ ((c & d) | (c & a) | (d & a)) &+ x[14] &+ 0x5a827999_u32) >> 19)
145+
146+
a = ((a &+ ((b & c) | (b & d) | (c & d)) &+ x[3] &+ 0x5a827999_u32) << 3) | ((a &+ ((b & c) | (b & d) | (c & d)) &+ x[3] &+ 0x5a827999_u32) >> 29)
147+
d = ((d &+ ((a & b) | (a & c) | (b & c)) &+ x[7] &+ 0x5a827999_u32) << 5) | ((d &+ ((a & b) | (a & c) | (b & c)) &+ x[7] &+ 0x5a827999_u32) >> 27)
148+
c = ((c &+ ((d & a) | (d & b) | (a & b)) &+ x[11] &+ 0x5a827999_u32) << 9) | ((c &+ ((d & a) | (d & b) | (a & b)) &+ x[11] &+ 0x5a827999_u32) >> 23)
149+
b = ((b &+ ((c & d) | (c & a) | (d & a)) &+ x[15] &+ 0x5a827999_u32) << 13) | ((b &+ ((c & d) | (c & a) | (d & a)) &+ x[15] &+ 0x5a827999_u32) >> 19)
150+
151+
# Round 3
152+
# [abcd k s]: a = (a + H(b,c,d) + X[k] + 0x6ed9eba1) <<< s
153+
# H(x,y,z) = x ^ y ^ z
154+
a = ((a &+ (b ^ c ^ d) &+ x[0] &+ 0x6ed9eba1_u32) << 3) | ((a &+ (b ^ c ^ d) &+ x[0] &+ 0x6ed9eba1_u32) >> 29)
155+
d = ((d &+ (a ^ b ^ c) &+ x[8] &+ 0x6ed9eba1_u32) << 9) | ((d &+ (a ^ b ^ c) &+ x[8] &+ 0x6ed9eba1_u32) >> 23)
156+
c = ((c &+ (d ^ a ^ b) &+ x[4] &+ 0x6ed9eba1_u32) << 11) | ((c &+ (d ^ a ^ b) &+ x[4] &+ 0x6ed9eba1_u32) >> 21)
157+
b = ((b &+ (c ^ d ^ a) &+ x[12] &+ 0x6ed9eba1_u32) << 15) | ((b &+ (c ^ d ^ a) &+ x[12] &+ 0x6ed9eba1_u32) >> 17)
158+
159+
a = ((a &+ (b ^ c ^ d) &+ x[2] &+ 0x6ed9eba1_u32) << 3) | ((a &+ (b ^ c ^ d) &+ x[2] &+ 0x6ed9eba1_u32) >> 29)
160+
d = ((d &+ (a ^ b ^ c) &+ x[10] &+ 0x6ed9eba1_u32) << 9) | ((d &+ (a ^ b ^ c) &+ x[10] &+ 0x6ed9eba1_u32) >> 23)
161+
c = ((c &+ (d ^ a ^ b) &+ x[6] &+ 0x6ed9eba1_u32) << 11) | ((c &+ (d ^ a ^ b) &+ x[6] &+ 0x6ed9eba1_u32) >> 21)
162+
b = ((b &+ (c ^ d ^ a) &+ x[14] &+ 0x6ed9eba1_u32) << 15) | ((b &+ (c ^ d ^ a) &+ x[14] &+ 0x6ed9eba1_u32) >> 17)
163+
164+
a = ((a &+ (b ^ c ^ d) &+ x[1] &+ 0x6ed9eba1_u32) << 3) | ((a &+ (b ^ c ^ d) &+ x[1] &+ 0x6ed9eba1_u32) >> 29)
165+
d = ((d &+ (a ^ b ^ c) &+ x[9] &+ 0x6ed9eba1_u32) << 9) | ((d &+ (a ^ b ^ c) &+ x[9] &+ 0x6ed9eba1_u32) >> 23)
166+
c = ((c &+ (d ^ a ^ b) &+ x[5] &+ 0x6ed9eba1_u32) << 11) | ((c &+ (d ^ a ^ b) &+ x[5] &+ 0x6ed9eba1_u32) >> 21)
167+
b = ((b &+ (c ^ d ^ a) &+ x[13] &+ 0x6ed9eba1_u32) << 15) | ((b &+ (c ^ d ^ a) &+ x[13] &+ 0x6ed9eba1_u32) >> 17)
168+
169+
a = ((a &+ (b ^ c ^ d) &+ x[3] &+ 0x6ed9eba1_u32) << 3) | ((a &+ (b ^ c ^ d) &+ x[3] &+ 0x6ed9eba1_u32) >> 29)
170+
d = ((d &+ (a ^ b ^ c) &+ x[11] &+ 0x6ed9eba1_u32) << 9) | ((d &+ (a ^ b ^ c) &+ x[11] &+ 0x6ed9eba1_u32) >> 23)
171+
c = ((c &+ (d ^ a ^ b) &+ x[7] &+ 0x6ed9eba1_u32) << 11) | ((c &+ (d ^ a ^ b) &+ x[7] &+ 0x6ed9eba1_u32) >> 21)
172+
b = ((b &+ (c ^ d ^ a) &+ x[15] &+ 0x6ed9eba1_u32) << 15) | ((b &+ (c ^ d ^ a) &+ x[15] &+ 0x6ed9eba1_u32) >> 17)
173+
174+
# Add back to state
175+
a = (a &+ aa) & 0xFFFFFFFF_u32
176+
b = (b &+ bb) & 0xFFFFFFFF_u32
177+
c = (c &+ cc) & 0xFFFFFFFF_u32
178+
d = (d &+ dd) & 0xFFFFFFFF_u32
179+
end
180+
181+
# Convert state to bytes (little endian)
182+
result = Bytes.new(16)
183+
184+
# a
185+
result[0] = (a & 0xFF).to_u8
186+
result[1] = ((a >> 8) & 0xFF).to_u8
187+
result[2] = ((a >> 16) & 0xFF).to_u8
188+
result[3] = ((a >> 24) & 0xFF).to_u8
189+
190+
# b
191+
result[4] = (b & 0xFF).to_u8
192+
result[5] = ((b >> 8) & 0xFF).to_u8
193+
result[6] = ((b >> 16) & 0xFF).to_u8
194+
result[7] = ((b >> 24) & 0xFF).to_u8
195+
196+
# c
197+
result[8] = (c & 0xFF).to_u8
198+
result[9] = ((c >> 8) & 0xFF).to_u8
199+
result[10] = ((c >> 16) & 0xFF).to_u8
200+
result[11] = ((c >> 24) & 0xFF).to_u8
201+
202+
# d
203+
result[12] = (d & 0xFF).to_u8
204+
result[13] = ((d >> 8) & 0xFF).to_u8
205+
result[14] = ((d >> 16) & 0xFF).to_u8
206+
result[15] = ((d >> 24) & 0xFF).to_u8
207+
208+
# Return hex string
209+
result.hexstring
210+
end
211+
18212
if file_path.empty?
19213
puts "⚠️ Error: No input file specified!"
20-
puts "Usage: hash-sentinel -f FILE"
214+
puts "Usage: hash-sentinel -f FILE [-w WORDLIST]"
21215
exit 1
22216
end
23217

24218
nt_hash_map = Hash(String, Array(String)).new { |hash, key| hash[key] = [] of String }
25219

220+
# Read NT hashes from input file
26221
begin
27222
File.each_line(file_path) do |line|
28223
line = line.strip
@@ -43,6 +238,48 @@ rescue ex : File::NotFoundError
43238
exit 1
44239
end
45240

241+
# If wordlist is provided, build hash-to-plaintext mapping
242+
password_matches = Hash(String, String).new
243+
244+
# Only process wordlist if a path was actually provided
245+
if !wordlist_path.empty?
246+
begin
247+
puts "📖 Reading wordlist and calculating hashes..."
248+
word_count = 0
249+
250+
# Check if file exists before trying to read it
251+
if !File.exists?(wordlist_path)
252+
puts "❌ Error: Wordlist file '#{wordlist_path}' not found!"
253+
exit 1
254+
end
255+
256+
# Process line by line to avoid loading entire file into memory
257+
begin
258+
word_count = 0
259+
File.each_line(wordlist_path) do |line|
260+
begin
261+
password = line.strip
262+
next if password.empty?
263+
264+
hash = calculate_nt_hash(password)
265+
password_matches[hash] = password
266+
word_count += 1
267+
rescue ex
268+
puts "⚠️ Warning: Skipping password '#{line.strip}': #{ex.message}"
269+
next
270+
end
271+
end
272+
puts "✓ Processed #{word_count} passwords from wordlist"
273+
rescue ex
274+
puts "❌ Error processing wordlist: #{ex.message}"
275+
exit 1
276+
end
277+
rescue ex
278+
puts "❌ Error reading wordlist: #{ex.message}"
279+
exit 1
280+
end
281+
end
282+
46283
puts "🔍 Analyzing NT hashes...\n"
47284

48285
duplicates_found = false
@@ -86,15 +323,25 @@ if duplicates_found
86323
end
87324
end
88325

89-
puts "╔═ #{usernames.size} accounts with identical passwords#{domain_text} ══"
326+
# Add plaintext password to header if found
327+
password_text = ""
328+
if !wordlist_path.empty? && password_matches.size > 0
329+
nt_hash = nt_hash_map.key_for(usernames)
330+
if password_matches.has_key?(nt_hash)
331+
plaintext = password_matches[nt_hash]
332+
password_text = " [Password: #{plaintext}]"
333+
end
334+
end
335+
336+
puts "╔═ #{usernames.size} accounts with identical passwords#{domain_text}#{password_text} ══"
90337

91338
# Display usernames without domains
92339
line = ""
93340
processed_usernames.each do |username|
94341
if line.empty?
95342
line = "║ • #{username}"
96343
else
97-
if line.size + username.size + 5 > 120
344+
if line.size + username.size + 5 > 100
98345
puts line
99346
line = "║ • #{username}"
100347
else
@@ -111,4 +358,4 @@ if duplicates_found
111358
puts "🔒 Recommendation: Ensure each account has a unique, strong password."
112359
else
113360
puts "✅ No users with duplicate passwords were found."
114-
end
361+
end

0 commit comments

Comments
 (0)