11require " option_parser"
22
33file_path = " "
4+ wordlist_path = " "
45
56OptionParser .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
1623end
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+
18212if 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
22216end
23217
24218nt_hash_map = Hash (String , Array (String )).new { |hash , key | hash[key] = [] of String }
25219
220+ # Read NT hashes from input file
26221begin
27222 File .each_line(file_path) do |line |
28223 line = line.strip
@@ -43,6 +238,48 @@ rescue ex : File::NotFoundError
43238 exit 1
44239end
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+
46283puts " 🔍 Analyzing NT hashes...\n "
47284
48285duplicates_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."
112359else
113360 puts " ✅ No users with duplicate passwords were found."
114- end
361+ end
0 commit comments