hashcrack-cli 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hashcrack/__init__.py +2 -0
- hashcrack/__main__.py +4 -0
- hashcrack/cli.py +700 -0
- hashcrack/detector.py +223 -0
- hashcrack/lookup.py +163 -0
- hashcrack_cli-0.1.1.dist-info/LICENSE +21 -0
- hashcrack_cli-0.1.1.dist-info/METADATA +128 -0
- hashcrack_cli-0.1.1.dist-info/RECORD +11 -0
- hashcrack_cli-0.1.1.dist-info/WHEEL +5 -0
- hashcrack_cli-0.1.1.dist-info/entry_points.txt +2 -0
- hashcrack_cli-0.1.1.dist-info/top_level.txt +1 -0
hashcrack/__init__.py
ADDED
hashcrack/__main__.py
ADDED
hashcrack/cli.py
ADDED
|
@@ -0,0 +1,700 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import hashlib
|
|
3
|
+
import json
|
|
4
|
+
import time
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
13
|
+
from rich.text import Text
|
|
14
|
+
from rich import box
|
|
15
|
+
from rich.columns import Columns
|
|
16
|
+
|
|
17
|
+
from hashcrack import __version__
|
|
18
|
+
from hashcrack.detector import detect_hash, classify_by_length, HASH_PATTERNS
|
|
19
|
+
from hashcrack.lookup import lookup_hash, generate_hash, verify_hash
|
|
20
|
+
|
|
21
|
+
console = Console()
|
|
22
|
+
err_console = Console(stderr=True)
|
|
23
|
+
|
|
24
|
+
CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
|
|
25
|
+
|
|
26
|
+
ALGO_CHOICES = [
|
|
27
|
+
"md5", "sha1", "sha224", "sha256", "sha384", "sha512",
|
|
28
|
+
"sha3-256", "sha3-512", "blake2b", "blake2s",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
TYPE_COLORS = {
|
|
32
|
+
"MD5": "cyan",
|
|
33
|
+
"SHA-1": "green",
|
|
34
|
+
"SHA-256": "bright_green",
|
|
35
|
+
"SHA-512": "bright_blue",
|
|
36
|
+
"bcrypt": "yellow",
|
|
37
|
+
"Argon2": "magenta",
|
|
38
|
+
"NTLM": "red",
|
|
39
|
+
"LM Hash": "bright_red",
|
|
40
|
+
"Base64": "white",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _color_for(name: str) -> str:
|
|
45
|
+
return TYPE_COLORS.get(name, "bright_white")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@click.group(context_settings=CONTEXT_SETTINGS)
|
|
49
|
+
@click.version_option(__version__, "-V", "--version", prog_name="hashcrack")
|
|
50
|
+
def cli():
|
|
51
|
+
"""hashcrack — Hash identification and lookup tool for CTF and security research.
|
|
52
|
+
|
|
53
|
+
\b
|
|
54
|
+
Identify hash types, look up plaintexts, generate hashes, and more.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ─────────────────────────────────────────────
|
|
59
|
+
# identify
|
|
60
|
+
# ─────────────────────────────────────────────
|
|
61
|
+
@cli.command("identify")
|
|
62
|
+
@click.argument("hash_string")
|
|
63
|
+
@click.option("--json", "output_json", is_flag=True, help="Output as JSON.")
|
|
64
|
+
def identify(hash_string: str, output_json: bool):
|
|
65
|
+
"""Identify the type(s) of a given hash.
|
|
66
|
+
|
|
67
|
+
\b
|
|
68
|
+
Examples:
|
|
69
|
+
hashcrack identify 5d41402abc4b2a76b9719d911017c592
|
|
70
|
+
hashcrack identify '$2a$12$R9h/cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ss7KIUgO2t0jWMUW'
|
|
71
|
+
"""
|
|
72
|
+
hash_string = hash_string.strip()
|
|
73
|
+
matches = detect_hash(hash_string)
|
|
74
|
+
|
|
75
|
+
if output_json:
|
|
76
|
+
click.echo(json.dumps({"hash": hash_string, "matches": matches}, indent=2))
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
length = len(hash_string)
|
|
80
|
+
console.print(Panel(
|
|
81
|
+
f"[bold yellow]{hash_string}[/bold yellow]",
|
|
82
|
+
title="[bold]Hash Input[/bold]",
|
|
83
|
+
subtitle=f"Length: {length} chars",
|
|
84
|
+
border_style="blue",
|
|
85
|
+
))
|
|
86
|
+
|
|
87
|
+
if not matches:
|
|
88
|
+
console.print("[red]No known hash type matched.[/red]")
|
|
89
|
+
console.print(f"[dim]Hash length {length} — try checking for encoding issues.[/dim]")
|
|
90
|
+
sys.exit(1)
|
|
91
|
+
|
|
92
|
+
table = Table(
|
|
93
|
+
title=f"[bold green]Possible Hash Types ({len(matches)} match{'es' if len(matches) != 1 else ''})[/bold green]",
|
|
94
|
+
box=box.ROUNDED,
|
|
95
|
+
border_style="green",
|
|
96
|
+
header_style="bold magenta",
|
|
97
|
+
)
|
|
98
|
+
table.add_column("Type", style="bold", min_width=14)
|
|
99
|
+
table.add_column("Description", style="dim")
|
|
100
|
+
table.add_column("Bits", justify="right", style="cyan")
|
|
101
|
+
|
|
102
|
+
bits_map = {32: 128, 40: 160, 48: 192, 56: 224, 64: 256, 96: 384, 128: 512}
|
|
103
|
+
|
|
104
|
+
for m in matches:
|
|
105
|
+
bits = bits_map.get(m.get("length", 0), "—") if m.get("length") else "variable"
|
|
106
|
+
color = _color_for(m["name"])
|
|
107
|
+
table.add_row(
|
|
108
|
+
f"[{color}]{m['name']}[/{color}]",
|
|
109
|
+
m["description"],
|
|
110
|
+
str(bits),
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
console.print(table)
|
|
114
|
+
|
|
115
|
+
if len(matches) > 1:
|
|
116
|
+
console.print(
|
|
117
|
+
"[dim]Tip: Multiple types share the same length. Use [bold]hashcrack lookup[/bold] "
|
|
118
|
+
"or context clues to narrow down.[/dim]"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# ─────────────────────────────────────────────
|
|
123
|
+
# lookup
|
|
124
|
+
# ─────────────────────────────────────────────
|
|
125
|
+
@cli.command("lookup")
|
|
126
|
+
@click.argument("hash_string")
|
|
127
|
+
@click.option("--json", "output_json", is_flag=True, help="Output as JSON.")
|
|
128
|
+
@click.option("--quiet", "-q", is_flag=True, help="Only print plaintext if found.")
|
|
129
|
+
def lookup(hash_string: str, output_json: bool, quiet: bool):
|
|
130
|
+
"""Look up plaintext for a hash via public databases.
|
|
131
|
+
|
|
132
|
+
\b
|
|
133
|
+
Searches multiple hash cracking APIs automatically.
|
|
134
|
+
|
|
135
|
+
\b
|
|
136
|
+
Examples:
|
|
137
|
+
hashcrack lookup 5d41402abc4b2a76b9719d911017c592
|
|
138
|
+
hashcrack lookup aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d
|
|
139
|
+
"""
|
|
140
|
+
hash_string = hash_string.strip()
|
|
141
|
+
|
|
142
|
+
if not quiet:
|
|
143
|
+
matches = detect_hash(hash_string)
|
|
144
|
+
types = ", ".join(m["name"] for m in matches) if matches else "Unknown"
|
|
145
|
+
console.print(Panel(
|
|
146
|
+
f"[bold yellow]{hash_string}[/bold yellow]\n[dim]Detected types: {types}[/dim]",
|
|
147
|
+
title="[bold]Hash Lookup[/bold]",
|
|
148
|
+
border_style="blue",
|
|
149
|
+
))
|
|
150
|
+
|
|
151
|
+
with Progress(
|
|
152
|
+
SpinnerColumn(),
|
|
153
|
+
TextColumn("[progress.description]{task.description}"),
|
|
154
|
+
transient=True,
|
|
155
|
+
console=console,
|
|
156
|
+
) as progress:
|
|
157
|
+
if not quiet:
|
|
158
|
+
task = progress.add_task("Querying hash databases...", total=None)
|
|
159
|
+
result = lookup_hash(hash_string)
|
|
160
|
+
|
|
161
|
+
if output_json:
|
|
162
|
+
click.echo(json.dumps({"hash": hash_string, **result}, indent=2))
|
|
163
|
+
return
|
|
164
|
+
|
|
165
|
+
if result["found"]:
|
|
166
|
+
if quiet:
|
|
167
|
+
click.echo(result["plaintext"])
|
|
168
|
+
else:
|
|
169
|
+
console.print(Panel(
|
|
170
|
+
f"[bold green]CRACKED![/bold green]\n\n"
|
|
171
|
+
f"Plaintext: [bold bright_green]{result['plaintext']}[/bold bright_green]\n"
|
|
172
|
+
f"Source: [dim]{result['source']}[/dim]",
|
|
173
|
+
title="[bold green]Result[/bold green]",
|
|
174
|
+
border_style="green",
|
|
175
|
+
))
|
|
176
|
+
else:
|
|
177
|
+
if quiet:
|
|
178
|
+
sys.exit(1)
|
|
179
|
+
else:
|
|
180
|
+
console.print(Panel(
|
|
181
|
+
"[red]Not found in public databases.[/red]\n\n"
|
|
182
|
+
"[dim]The hash may be:\n"
|
|
183
|
+
" • Using a strong algorithm (bcrypt, Argon2, scrypt)\n"
|
|
184
|
+
" • A custom or salted hash\n"
|
|
185
|
+
" • Not in any public rainbow table[/dim]",
|
|
186
|
+
title="[bold red]Not Found[/bold red]",
|
|
187
|
+
border_style="red",
|
|
188
|
+
))
|
|
189
|
+
sys.exit(1)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# ─────────────────────────────────────────────
|
|
193
|
+
# generate
|
|
194
|
+
# ─────────────────────────────────────────────
|
|
195
|
+
@cli.command("generate")
|
|
196
|
+
@click.argument("text")
|
|
197
|
+
@click.option(
|
|
198
|
+
"--algo", "-a",
|
|
199
|
+
multiple=True,
|
|
200
|
+
type=click.Choice(ALGO_CHOICES, case_sensitive=False),
|
|
201
|
+
help="Algorithm(s) to use. Repeat for multiple. Default: all common.",
|
|
202
|
+
)
|
|
203
|
+
@click.option("--all", "all_algos", is_flag=True, help="Generate all supported hashes.")
|
|
204
|
+
@click.option("--json", "output_json", is_flag=True, help="Output as JSON.")
|
|
205
|
+
def generate(text: str, algo: tuple, all_algos: bool, output_json: bool):
|
|
206
|
+
"""Generate hash(es) for a given string.
|
|
207
|
+
|
|
208
|
+
\b
|
|
209
|
+
Examples:
|
|
210
|
+
hashcrack generate "hello world"
|
|
211
|
+
hashcrack generate "password" --algo md5 --algo sha256
|
|
212
|
+
hashcrack generate "secret" --all
|
|
213
|
+
"""
|
|
214
|
+
if all_algos or not algo:
|
|
215
|
+
algorithms = ALGO_CHOICES
|
|
216
|
+
else:
|
|
217
|
+
algorithms = list(algo)
|
|
218
|
+
|
|
219
|
+
results = {}
|
|
220
|
+
for a in algorithms:
|
|
221
|
+
h = generate_hash(text, a)
|
|
222
|
+
if h:
|
|
223
|
+
results[a] = h
|
|
224
|
+
|
|
225
|
+
if output_json:
|
|
226
|
+
click.echo(json.dumps({"input": text, "hashes": results}, indent=2))
|
|
227
|
+
return
|
|
228
|
+
|
|
229
|
+
table = Table(
|
|
230
|
+
title=f"[bold]Hashes for:[/bold] [yellow]{text}[/yellow]",
|
|
231
|
+
box=box.ROUNDED,
|
|
232
|
+
border_style="cyan",
|
|
233
|
+
header_style="bold magenta",
|
|
234
|
+
)
|
|
235
|
+
table.add_column("Algorithm", style="bold cyan", min_width=12)
|
|
236
|
+
table.add_column("Hash", style="white")
|
|
237
|
+
|
|
238
|
+
for algo_name, hash_val in results.items():
|
|
239
|
+
table.add_row(algo_name.upper(), hash_val)
|
|
240
|
+
|
|
241
|
+
console.print(table)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
# ─────────────────────────────────────────────
|
|
245
|
+
# batch
|
|
246
|
+
# ─────────────────────────────────────────────
|
|
247
|
+
@cli.command("batch")
|
|
248
|
+
@click.argument("input_file", type=click.Path(exists=True))
|
|
249
|
+
@click.option("--lookup", "do_lookup", is_flag=True, help="Also attempt to look up plaintexts.")
|
|
250
|
+
@click.option("--output", "-o", type=click.Path(), help="Save results to a file (JSON).")
|
|
251
|
+
@click.option("--json", "output_json", is_flag=True, help="Output as JSON.")
|
|
252
|
+
def batch(input_file: str, do_lookup: bool, output: Optional[str], output_json: bool):
|
|
253
|
+
"""Process multiple hashes from a file (one per line).
|
|
254
|
+
|
|
255
|
+
\b
|
|
256
|
+
Examples:
|
|
257
|
+
hashcrack batch hashes.txt
|
|
258
|
+
hashcrack batch hashes.txt --lookup
|
|
259
|
+
hashcrack batch hashes.txt --lookup --output results.json
|
|
260
|
+
"""
|
|
261
|
+
path = Path(input_file)
|
|
262
|
+
lines = [l.strip() for l in path.read_text().splitlines() if l.strip() and not l.startswith("#")]
|
|
263
|
+
|
|
264
|
+
if not lines:
|
|
265
|
+
if output_json:
|
|
266
|
+
click.echo("[]")
|
|
267
|
+
return
|
|
268
|
+
console.print("[red]No hashes found in file.[/red]")
|
|
269
|
+
sys.exit(1)
|
|
270
|
+
|
|
271
|
+
if not output_json:
|
|
272
|
+
console.print(Panel(
|
|
273
|
+
f"Processing [bold]{len(lines)}[/bold] hash(es) from [cyan]{path.name}[/cyan]",
|
|
274
|
+
title="[bold]Batch Mode[/bold]",
|
|
275
|
+
border_style="blue",
|
|
276
|
+
))
|
|
277
|
+
|
|
278
|
+
all_results = []
|
|
279
|
+
|
|
280
|
+
if output_json:
|
|
281
|
+
for line in lines:
|
|
282
|
+
matches = detect_hash(line)
|
|
283
|
+
entry = {
|
|
284
|
+
"hash": line,
|
|
285
|
+
"types": [m["name"] for m in matches],
|
|
286
|
+
"found": False,
|
|
287
|
+
"plaintext": None,
|
|
288
|
+
"source": None,
|
|
289
|
+
}
|
|
290
|
+
if do_lookup and matches:
|
|
291
|
+
result = lookup_hash(line)
|
|
292
|
+
entry.update(result)
|
|
293
|
+
all_results.append(entry)
|
|
294
|
+
else:
|
|
295
|
+
with Progress(
|
|
296
|
+
SpinnerColumn(),
|
|
297
|
+
TextColumn("[progress.description]{task.description}"),
|
|
298
|
+
console=console,
|
|
299
|
+
) as progress:
|
|
300
|
+
task = progress.add_task("Processing...", total=len(lines))
|
|
301
|
+
|
|
302
|
+
for i, line in enumerate(lines, 1):
|
|
303
|
+
progress.update(task, description=f"[{i}/{len(lines)}] {line[:32]}...")
|
|
304
|
+
matches = detect_hash(line)
|
|
305
|
+
entry = {
|
|
306
|
+
"hash": line,
|
|
307
|
+
"types": [m["name"] for m in matches],
|
|
308
|
+
"found": False,
|
|
309
|
+
"plaintext": None,
|
|
310
|
+
"source": None,
|
|
311
|
+
}
|
|
312
|
+
if do_lookup and matches:
|
|
313
|
+
result = lookup_hash(line)
|
|
314
|
+
entry.update(result)
|
|
315
|
+
all_results.append(entry)
|
|
316
|
+
progress.advance(task)
|
|
317
|
+
|
|
318
|
+
if output_json:
|
|
319
|
+
click.echo(json.dumps(all_results, indent=2))
|
|
320
|
+
else:
|
|
321
|
+
table = Table(
|
|
322
|
+
title=f"[bold]Batch Results — {len(all_results)} hashes[/bold]",
|
|
323
|
+
box=box.ROUNDED,
|
|
324
|
+
border_style="cyan",
|
|
325
|
+
header_style="bold magenta",
|
|
326
|
+
)
|
|
327
|
+
table.add_column("Hash", style="dim", max_width=40)
|
|
328
|
+
table.add_column("Detected Types", style="cyan")
|
|
329
|
+
if do_lookup:
|
|
330
|
+
table.add_column("Plaintext", style="bold green")
|
|
331
|
+
table.add_column("Source", style="dim")
|
|
332
|
+
|
|
333
|
+
for entry in all_results:
|
|
334
|
+
short_hash = entry["hash"][:36] + "..." if len(entry["hash"]) > 36 else entry["hash"]
|
|
335
|
+
types_str = ", ".join(entry["types"]) if entry["types"] else "[red]Unknown[/red]"
|
|
336
|
+
if do_lookup:
|
|
337
|
+
plaintext = f"[green]{entry['plaintext']}[/green]" if entry["found"] else "[red]Not found[/red]"
|
|
338
|
+
source = entry["source"] or ""
|
|
339
|
+
table.add_row(short_hash, types_str, plaintext, source)
|
|
340
|
+
else:
|
|
341
|
+
table.add_row(short_hash, types_str)
|
|
342
|
+
|
|
343
|
+
console.print(table)
|
|
344
|
+
|
|
345
|
+
if output:
|
|
346
|
+
Path(output).write_text(json.dumps(all_results, indent=2))
|
|
347
|
+
if not output_json:
|
|
348
|
+
console.print(f"\n[dim]Results saved to [cyan]{output}[/cyan][/dim]")
|
|
349
|
+
|
|
350
|
+
if do_lookup and not output_json:
|
|
351
|
+
found_count = sum(1 for r in all_results if r["found"])
|
|
352
|
+
console.print(
|
|
353
|
+
f"\n[bold]Summary:[/bold] {found_count}/{len(all_results)} hashes cracked "
|
|
354
|
+
f"[dim]({100*found_count//len(all_results) if all_results else 0}%)[/dim]"
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
# ─────────────────────────────────────────────
|
|
359
|
+
# verify
|
|
360
|
+
# ─────────────────────────────────────────────
|
|
361
|
+
@cli.command("verify")
|
|
362
|
+
@click.argument("hash_string")
|
|
363
|
+
@click.argument("plaintext")
|
|
364
|
+
@click.option(
|
|
365
|
+
"--algo", "-a",
|
|
366
|
+
type=click.Choice(ALGO_CHOICES, case_sensitive=False),
|
|
367
|
+
help="Force a specific algorithm.",
|
|
368
|
+
)
|
|
369
|
+
@click.option("--json", "output_json", is_flag=True, help="Output as JSON.")
|
|
370
|
+
def verify(hash_string: str, plaintext: str, algo: Optional[str], output_json: bool):
|
|
371
|
+
"""Verify that a plaintext matches a hash.
|
|
372
|
+
|
|
373
|
+
\b
|
|
374
|
+
Examples:
|
|
375
|
+
hashcrack verify 5d41402abc4b2a76b9719d911017c592 "hello"
|
|
376
|
+
hashcrack verify <sha256_hash> "password" --algo sha256
|
|
377
|
+
"""
|
|
378
|
+
hash_string = hash_string.strip()
|
|
379
|
+
|
|
380
|
+
if algo:
|
|
381
|
+
algorithms_to_try = [algo]
|
|
382
|
+
else:
|
|
383
|
+
matches = detect_hash(hash_string)
|
|
384
|
+
if not matches:
|
|
385
|
+
err_console.print("[red]Cannot detect hash type. Use --algo to specify.[/red]")
|
|
386
|
+
sys.exit(1)
|
|
387
|
+
algorithms_to_try = [m["name"].lower().replace("-", "").replace(" ", "")
|
|
388
|
+
for m in matches if m.get("length")]
|
|
389
|
+
|
|
390
|
+
verified = False
|
|
391
|
+
matched_algo = None
|
|
392
|
+
|
|
393
|
+
for a in algorithms_to_try:
|
|
394
|
+
clean_algo = a.lower().replace("-", "").replace("_", "").replace(" ", "")
|
|
395
|
+
if verify_hash(hash_string, plaintext, clean_algo):
|
|
396
|
+
verified = True
|
|
397
|
+
matched_algo = a
|
|
398
|
+
break
|
|
399
|
+
|
|
400
|
+
if output_json:
|
|
401
|
+
click.echo(json.dumps({
|
|
402
|
+
"hash": hash_string,
|
|
403
|
+
"plaintext": plaintext,
|
|
404
|
+
"verified": verified,
|
|
405
|
+
"algorithm": matched_algo,
|
|
406
|
+
}, indent=2))
|
|
407
|
+
return
|
|
408
|
+
|
|
409
|
+
if verified:
|
|
410
|
+
console.print(Panel(
|
|
411
|
+
f"[bold green]MATCH![/bold green]\n\n"
|
|
412
|
+
f"Hash: [dim]{hash_string}[/dim]\n"
|
|
413
|
+
f"Plaintext: [bold green]{plaintext}[/bold green]\n"
|
|
414
|
+
f"Algorithm: [cyan]{matched_algo}[/cyan]",
|
|
415
|
+
title="[bold green]Verification[/bold green]",
|
|
416
|
+
border_style="green",
|
|
417
|
+
))
|
|
418
|
+
else:
|
|
419
|
+
console.print(Panel(
|
|
420
|
+
f"[bold red]NO MATCH[/bold red]\n\n"
|
|
421
|
+
f"Hash: [dim]{hash_string}[/dim]\n"
|
|
422
|
+
f"Plaintext: [yellow]{plaintext}[/yellow]\n"
|
|
423
|
+
f"Tried: [dim]{', '.join(algorithms_to_try)}[/dim]",
|
|
424
|
+
title="[bold red]Verification Failed[/bold red]",
|
|
425
|
+
border_style="red",
|
|
426
|
+
))
|
|
427
|
+
sys.exit(1)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
# ─────────────────────────────────────────────
|
|
431
|
+
# info
|
|
432
|
+
# ─────────────────────────────────────────────
|
|
433
|
+
@cli.command("info")
|
|
434
|
+
@click.argument("hash_type", required=False)
|
|
435
|
+
@click.option("--json", "output_json", is_flag=True, help="Output as JSON.")
|
|
436
|
+
def info(hash_type: Optional[str], output_json: bool):
|
|
437
|
+
"""Show information about a hash algorithm, or list all supported types.
|
|
438
|
+
|
|
439
|
+
\b
|
|
440
|
+
Examples:
|
|
441
|
+
hashcrack info # list all supported hash types
|
|
442
|
+
hashcrack info md5 # details on MD5
|
|
443
|
+
hashcrack info sha256
|
|
444
|
+
"""
|
|
445
|
+
if hash_type:
|
|
446
|
+
target = hash_type.upper().replace("_", "-").replace(" ", "-")
|
|
447
|
+
matches = [p for p in HASH_PATTERNS if p["name"].upper() == target]
|
|
448
|
+
|
|
449
|
+
if not matches:
|
|
450
|
+
# Try fuzzy
|
|
451
|
+
matches = [p for p in HASH_PATTERNS if target in p["name"].upper()]
|
|
452
|
+
|
|
453
|
+
if not matches:
|
|
454
|
+
err_console.print(f"[red]Unknown hash type: {hash_type}[/red]")
|
|
455
|
+
err_console.print("[dim]Run [bold]hashcrack info[/bold] to list all types.[/dim]")
|
|
456
|
+
sys.exit(1)
|
|
457
|
+
|
|
458
|
+
m = matches[0]
|
|
459
|
+
if output_json:
|
|
460
|
+
click.echo(json.dumps(m, indent=2))
|
|
461
|
+
return
|
|
462
|
+
|
|
463
|
+
bits_map = {32: 128, 40: 160, 48: 192, 56: 224, 64: 256, 96: 384, 128: 512}
|
|
464
|
+
bits = bits_map.get(m.get("length", 0), "variable")
|
|
465
|
+
color = _color_for(m["name"])
|
|
466
|
+
|
|
467
|
+
console.print(Panel(
|
|
468
|
+
f"[bold {color}]{m['name']}[/bold {color}]\n\n"
|
|
469
|
+
f"[dim]Description:[/dim] {m['description']}\n"
|
|
470
|
+
f"[dim]Hex length:[/dim] {m.get('length', 'variable')}\n"
|
|
471
|
+
f"[dim]Bits:[/dim] {bits}\n"
|
|
472
|
+
f"[dim]Pattern:[/dim] [dim]{m['regex']}[/dim]\n\n"
|
|
473
|
+
f"[dim]Example:[/dim]\n[yellow]{m['example']}[/yellow]",
|
|
474
|
+
title=f"[bold]Hash Info — {m['name']}[/bold]",
|
|
475
|
+
border_style=color,
|
|
476
|
+
))
|
|
477
|
+
else:
|
|
478
|
+
if output_json:
|
|
479
|
+
click.echo(json.dumps(HASH_PATTERNS, indent=2))
|
|
480
|
+
return
|
|
481
|
+
|
|
482
|
+
table = Table(
|
|
483
|
+
title="[bold]Supported Hash Types[/bold]",
|
|
484
|
+
box=box.ROUNDED,
|
|
485
|
+
border_style="blue",
|
|
486
|
+
header_style="bold magenta",
|
|
487
|
+
)
|
|
488
|
+
table.add_column("Type", style="bold", min_width=14)
|
|
489
|
+
table.add_column("Bits", justify="right", style="cyan")
|
|
490
|
+
table.add_column("Hex Length", justify="right", style="dim")
|
|
491
|
+
table.add_column("Description", style="white")
|
|
492
|
+
|
|
493
|
+
bits_map = {32: 128, 40: 160, 48: 192, 56: 224, 64: 256, 96: 384, 128: 512}
|
|
494
|
+
|
|
495
|
+
for p in HASH_PATTERNS:
|
|
496
|
+
bits = bits_map.get(p.get("length", 0), "var")
|
|
497
|
+
color = _color_for(p["name"])
|
|
498
|
+
table.add_row(
|
|
499
|
+
f"[{color}]{p['name']}[/{color}]",
|
|
500
|
+
str(bits),
|
|
501
|
+
str(p.get("length", "—")),
|
|
502
|
+
p["description"],
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
console.print(table)
|
|
506
|
+
console.print(f"\n[dim]Total: {len(HASH_PATTERNS)} hash types. Run [bold]hashcrack info <type>[/bold] for details.[/dim]")
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
# ─────────────────────────────────────────────
|
|
510
|
+
# analyze
|
|
511
|
+
# ─────────────────────────────────────────────
|
|
512
|
+
@cli.command("analyze")
|
|
513
|
+
@click.argument("hash_string")
|
|
514
|
+
@click.option("--json", "output_json", is_flag=True, help="Output as JSON.")
|
|
515
|
+
def analyze(hash_string: str, output_json: bool):
|
|
516
|
+
"""Deep analysis of a hash: type detection + lookup + entropy.
|
|
517
|
+
|
|
518
|
+
\b
|
|
519
|
+
Combines identify, lookup, and statistical analysis in one command.
|
|
520
|
+
|
|
521
|
+
\b
|
|
522
|
+
Examples:
|
|
523
|
+
hashcrack analyze 5d41402abc4b2a76b9719d911017c592
|
|
524
|
+
"""
|
|
525
|
+
import math
|
|
526
|
+
|
|
527
|
+
hash_string = hash_string.strip()
|
|
528
|
+
matches = detect_hash(hash_string)
|
|
529
|
+
|
|
530
|
+
# Entropy calculation
|
|
531
|
+
freq = {}
|
|
532
|
+
for ch in hash_string.lower():
|
|
533
|
+
freq[ch] = freq.get(ch, 0) + 1
|
|
534
|
+
length = len(hash_string)
|
|
535
|
+
entropy = 0.0
|
|
536
|
+
for count in freq.values():
|
|
537
|
+
p = count / length
|
|
538
|
+
if p > 0:
|
|
539
|
+
entropy -= p * math.log2(p)
|
|
540
|
+
|
|
541
|
+
# Character distribution
|
|
542
|
+
hex_chars = sum(1 for c in hash_string if c in "0123456789abcdefABCDEF")
|
|
543
|
+
is_pure_hex = hex_chars == length
|
|
544
|
+
|
|
545
|
+
result = {
|
|
546
|
+
"hash": hash_string,
|
|
547
|
+
"length": length,
|
|
548
|
+
"entropy": round(entropy, 4),
|
|
549
|
+
"is_pure_hex": is_pure_hex,
|
|
550
|
+
"detected_types": [m["name"] for m in matches],
|
|
551
|
+
"lookup": None,
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
console.print(Panel(
|
|
555
|
+
f"[bold yellow]{hash_string}[/bold yellow]",
|
|
556
|
+
title="[bold]Deep Hash Analysis[/bold]",
|
|
557
|
+
border_style="magenta",
|
|
558
|
+
))
|
|
559
|
+
|
|
560
|
+
# Stats table
|
|
561
|
+
stats = Table(box=box.SIMPLE, show_header=False)
|
|
562
|
+
stats.add_column("Property", style="dim")
|
|
563
|
+
stats.add_column("Value", style="bold")
|
|
564
|
+
|
|
565
|
+
stats.add_row("Length", f"{length} characters")
|
|
566
|
+
stats.add_row("Entropy", f"{entropy:.2f} bits/char")
|
|
567
|
+
stats.add_row("Pure hex", "Yes" if is_pure_hex else "No")
|
|
568
|
+
stats.add_row("Charset", _describe_charset(hash_string))
|
|
569
|
+
|
|
570
|
+
console.print(stats)
|
|
571
|
+
|
|
572
|
+
if matches:
|
|
573
|
+
console.print(f"\n[bold green]Detected Types:[/bold green]")
|
|
574
|
+
for m in matches:
|
|
575
|
+
color = _color_for(m["name"])
|
|
576
|
+
console.print(f" [{color}]●[/{color}] [bold]{m['name']}[/bold] — {m['description']}")
|
|
577
|
+
else:
|
|
578
|
+
console.print("[yellow]No known hash type matched.[/yellow]")
|
|
579
|
+
|
|
580
|
+
# Attempt lookup for hex hashes
|
|
581
|
+
if is_pure_hex and length in (32, 40, 64, 128):
|
|
582
|
+
console.print("\n[dim]Attempting lookup in public databases...[/dim]")
|
|
583
|
+
lookup_result = lookup_hash(hash_string)
|
|
584
|
+
result["lookup"] = lookup_result
|
|
585
|
+
if lookup_result["found"]:
|
|
586
|
+
console.print(Panel(
|
|
587
|
+
f"[bold green]CRACKED![/bold green] Plaintext: [bright_green]{lookup_result['plaintext']}[/bright_green]\n"
|
|
588
|
+
f"[dim]Source: {lookup_result['source']}[/dim]",
|
|
589
|
+
border_style="green",
|
|
590
|
+
))
|
|
591
|
+
else:
|
|
592
|
+
console.print("[dim]Not found in public databases.[/dim]")
|
|
593
|
+
|
|
594
|
+
if output_json:
|
|
595
|
+
click.echo(json.dumps(result, indent=2))
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def _describe_charset(s: str) -> str:
|
|
599
|
+
has_lower = any(c.islower() for c in s)
|
|
600
|
+
has_upper = any(c.isupper() for c in s)
|
|
601
|
+
has_digit = any(c.isdigit() for c in s)
|
|
602
|
+
has_special = any(not c.isalnum() for c in s)
|
|
603
|
+
parts = []
|
|
604
|
+
if has_lower:
|
|
605
|
+
parts.append("lowercase")
|
|
606
|
+
if has_upper:
|
|
607
|
+
parts.append("uppercase")
|
|
608
|
+
if has_digit:
|
|
609
|
+
parts.append("digits")
|
|
610
|
+
if has_special:
|
|
611
|
+
parts.append("special")
|
|
612
|
+
return ", ".join(parts) if parts else "unknown"
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
# ─────────────────────────────────────────────
|
|
616
|
+
# wordlist
|
|
617
|
+
# ─────────────────────────────────────────────
|
|
618
|
+
@cli.command("wordlist")
|
|
619
|
+
@click.argument("wordlist_file", type=click.Path(exists=True))
|
|
620
|
+
@click.argument("hash_string")
|
|
621
|
+
@click.option(
|
|
622
|
+
"--algo", "-a",
|
|
623
|
+
type=click.Choice(ALGO_CHOICES, case_sensitive=False),
|
|
624
|
+
default="md5",
|
|
625
|
+
show_default=True,
|
|
626
|
+
help="Hash algorithm to use.",
|
|
627
|
+
)
|
|
628
|
+
@click.option("--json", "output_json", is_flag=True, help="Output as JSON.")
|
|
629
|
+
def wordlist(wordlist_file: str, hash_string: str, algo: str, output_json: bool):
|
|
630
|
+
"""Crack a hash using a local wordlist file.
|
|
631
|
+
|
|
632
|
+
\b
|
|
633
|
+
Hashes each line of the wordlist and compares against the target.
|
|
634
|
+
|
|
635
|
+
\b
|
|
636
|
+
Examples:
|
|
637
|
+
hashcrack wordlist rockyou.txt 5d41402abc4b2a76b9719d911017c592
|
|
638
|
+
hashcrack wordlist passwords.txt <sha256_hash> --algo sha256
|
|
639
|
+
"""
|
|
640
|
+
hash_string = hash_string.strip().lower()
|
|
641
|
+
path = Path(wordlist_file)
|
|
642
|
+
|
|
643
|
+
total_lines = sum(1 for _ in path.open("r", errors="ignore"))
|
|
644
|
+
|
|
645
|
+
console.print(Panel(
|
|
646
|
+
f"Target: [bold yellow]{hash_string}[/bold yellow]\n"
|
|
647
|
+
f"Algorithm: [cyan]{algo.upper()}[/cyan]\n"
|
|
648
|
+
f"Wordlist: [dim]{path.name}[/dim] ([bold]{total_lines:,}[/bold] words)",
|
|
649
|
+
title="[bold]Wordlist Attack[/bold]",
|
|
650
|
+
border_style="blue",
|
|
651
|
+
))
|
|
652
|
+
|
|
653
|
+
found = False
|
|
654
|
+
plaintext = None
|
|
655
|
+
tried = 0
|
|
656
|
+
|
|
657
|
+
with Progress(
|
|
658
|
+
SpinnerColumn(),
|
|
659
|
+
TextColumn("[progress.description]{task.description}"),
|
|
660
|
+
console=console,
|
|
661
|
+
) as progress:
|
|
662
|
+
task = progress.add_task("Cracking...", total=total_lines)
|
|
663
|
+
|
|
664
|
+
with path.open("r", errors="ignore") as f:
|
|
665
|
+
for line in f:
|
|
666
|
+
word = line.rstrip("\n\r")
|
|
667
|
+
computed = generate_hash(word, algo)
|
|
668
|
+
tried += 1
|
|
669
|
+
progress.advance(task)
|
|
670
|
+
|
|
671
|
+
if computed and computed.lower() == hash_string:
|
|
672
|
+
found = True
|
|
673
|
+
plaintext = word
|
|
674
|
+
break
|
|
675
|
+
|
|
676
|
+
if output_json:
|
|
677
|
+
click.echo(json.dumps({
|
|
678
|
+
"hash": hash_string,
|
|
679
|
+
"algorithm": algo,
|
|
680
|
+
"found": found,
|
|
681
|
+
"plaintext": plaintext,
|
|
682
|
+
"tried": tried,
|
|
683
|
+
}, indent=2))
|
|
684
|
+
return
|
|
685
|
+
|
|
686
|
+
if found:
|
|
687
|
+
console.print(Panel(
|
|
688
|
+
f"[bold green]CRACKED![/bold green]\n\n"
|
|
689
|
+
f"Plaintext: [bold bright_green]{plaintext}[/bold bright_green]\n"
|
|
690
|
+
f"Tried: [dim]{tried:,} words[/dim]",
|
|
691
|
+
title="[bold green]Success[/bold green]",
|
|
692
|
+
border_style="green",
|
|
693
|
+
))
|
|
694
|
+
else:
|
|
695
|
+
console.print(Panel(
|
|
696
|
+
f"[red]Not found in wordlist.[/red]\n[dim]Tried {tried:,} words.[/dim]",
|
|
697
|
+
title="[bold red]Not Found[/bold red]",
|
|
698
|
+
border_style="red",
|
|
699
|
+
))
|
|
700
|
+
sys.exit(1)
|
hashcrack/detector.py
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
HASH_PATTERNS = [
|
|
5
|
+
{
|
|
6
|
+
"name": "MD5",
|
|
7
|
+
"regex": r"^[a-f0-9]{32}$",
|
|
8
|
+
"length": 32,
|
|
9
|
+
"description": "Message Digest 5",
|
|
10
|
+
"example": "5d41402abc4b2a76b9719d911017c592",
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"name": "SHA-1",
|
|
14
|
+
"regex": r"^[a-f0-9]{40}$",
|
|
15
|
+
"length": 40,
|
|
16
|
+
"description": "Secure Hash Algorithm 1",
|
|
17
|
+
"example": "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d",
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"name": "SHA-224",
|
|
21
|
+
"regex": r"^[a-f0-9]{56}$",
|
|
22
|
+
"length": 56,
|
|
23
|
+
"description": "Secure Hash Algorithm 224",
|
|
24
|
+
"example": "abd37534c7d9a2efb9465de931cd7055ffdb8879563ae98078d6d6d5",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"name": "SHA-256",
|
|
28
|
+
"regex": r"^[a-f0-9]{64}$",
|
|
29
|
+
"length": 64,
|
|
30
|
+
"description": "Secure Hash Algorithm 256",
|
|
31
|
+
"example": "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"name": "SHA-384",
|
|
35
|
+
"regex": r"^[a-f0-9]{96}$",
|
|
36
|
+
"length": 96,
|
|
37
|
+
"description": "Secure Hash Algorithm 384",
|
|
38
|
+
"example": "59e1748777448c69de6b800d7a33bbfb9ff1b463e44354c3553bcdb9c666fa90125a3c79f90397bdf5f6a13de828684f",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"name": "SHA-512",
|
|
42
|
+
"regex": r"^[a-f0-9]{128}$",
|
|
43
|
+
"length": 128,
|
|
44
|
+
"description": "Secure Hash Algorithm 512",
|
|
45
|
+
"example": "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"name": "SHA3-256",
|
|
49
|
+
"regex": r"^[a-f0-9]{64}$",
|
|
50
|
+
"length": 64,
|
|
51
|
+
"description": "SHA3 256-bit (Keccak)",
|
|
52
|
+
"example": "a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"name": "SHA3-512",
|
|
56
|
+
"regex": r"^[a-f0-9]{128}$",
|
|
57
|
+
"length": 128,
|
|
58
|
+
"description": "SHA3 512-bit (Keccak)",
|
|
59
|
+
"example": "a69f73cca23a9ac5c8b567dc185a756e97c982164fe25859e0d1dcc1475c80a615b2123af1f5f94c11e3e9402c3ac558f500199d95b6d3e301758586281dcd26",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"name": "NTLM",
|
|
63
|
+
"regex": r"^[a-f0-9]{32}$",
|
|
64
|
+
"length": 32,
|
|
65
|
+
"description": "NT LAN Manager hash (Windows auth)",
|
|
66
|
+
"example": "b4b9b02e6f09a9bd760f388b67351e2b",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"name": "MySQL4/5",
|
|
70
|
+
"regex": r"^\*[a-f0-9]{40}$",
|
|
71
|
+
"length": 41,
|
|
72
|
+
"description": "MySQL 4.1+ password hash",
|
|
73
|
+
"example": "*900ADE9CA5AE4B40B0E6A8D4E1FEC32DFEA38E4E",
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"name": "bcrypt",
|
|
77
|
+
"regex": r"^\$2[ayb]\$.{56}$",
|
|
78
|
+
"length": None,
|
|
79
|
+
"description": "bcrypt password hash",
|
|
80
|
+
"example": "$2a$12$R9h/cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ss7KIUgO2t0jWMUW",
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"name": "Argon2",
|
|
84
|
+
"regex": r"^\$argon2(i|d|id)\$",
|
|
85
|
+
"length": None,
|
|
86
|
+
"description": "Argon2 password hash",
|
|
87
|
+
"example": "$argon2id$v=19$m=65536,t=2,p=1$c29tZXNhbHQ$RdescudvJCsgt3ub+b+dWRWJTmaaJObG",
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"name": "scrypt",
|
|
91
|
+
"regex": r"^\$scrypt\$",
|
|
92
|
+
"length": None,
|
|
93
|
+
"description": "scrypt password hash",
|
|
94
|
+
"example": "$scrypt$ln=16,r=8,p=1$aM15713r3Xsvxbi31lqr1Q$nFNh2CVHVjNldFVKDHDlm4CbdRSCdEBsjjJxD+iCs5E",
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"name": "MD5 Crypt",
|
|
98
|
+
"regex": r"^\$1\$.{8}\$.{22}$",
|
|
99
|
+
"length": None,
|
|
100
|
+
"description": "MD5 Unix Crypt",
|
|
101
|
+
"example": "$1$O3JMY.Tw$AdLnLjQ/5jXF9.MTp3gHv/",
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"name": "SHA-512 Crypt",
|
|
105
|
+
"regex": r"^\$6\$.{8,16}\$.{86}$",
|
|
106
|
+
"length": None,
|
|
107
|
+
"description": "SHA-512 Unix Crypt",
|
|
108
|
+
"example": "$6$52450745$k5ka2p8bFuSmoVT1tzOyyuaREkkKBcCNqoDKzYiJL9RaE8yMnPgh2XzzF0NDrUhgrcLwg78xs1w5pJiypEdFX/",
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"name": "BLAKE2b",
|
|
112
|
+
"regex": r"^[a-f0-9]{128}$",
|
|
113
|
+
"length": 128,
|
|
114
|
+
"description": "BLAKE2b 512-bit",
|
|
115
|
+
"example": "786a02f742015903c6c6fd852552d272912f4740e15847618a86e217f71f5419d25e1031afee585313896444934eb04b903a685b1448b755d56f701afe9be2ce",
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"name": "BLAKE2s",
|
|
119
|
+
"regex": r"^[a-f0-9]{64}$",
|
|
120
|
+
"length": 64,
|
|
121
|
+
"description": "BLAKE2s 256-bit",
|
|
122
|
+
"example": "606beeec743ccbeff6cbcdf5d5302aa855c256c29b88c8ed331ea1a6bf3c8812",
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"name": "CRC32",
|
|
126
|
+
"regex": r"^[a-f0-9]{8}$",
|
|
127
|
+
"length": 8,
|
|
128
|
+
"description": "CRC32 cyclic redundancy check",
|
|
129
|
+
"example": "fc4f1d7f",
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"name": "Adler32",
|
|
133
|
+
"regex": r"^[a-f0-9]{8}$",
|
|
134
|
+
"length": 8,
|
|
135
|
+
"description": "Adler-32 checksum",
|
|
136
|
+
"example": "03da018b",
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
"name": "RIPEMD-160",
|
|
140
|
+
"regex": r"^[a-f0-9]{40}$",
|
|
141
|
+
"length": 40,
|
|
142
|
+
"description": "RACE Integrity Primitives Evaluation MD 160-bit",
|
|
143
|
+
"example": "108f07b8382412612c048d07d13f814118445acd",
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
"name": "Whirlpool",
|
|
147
|
+
"regex": r"^[a-f0-9]{128}$",
|
|
148
|
+
"length": 128,
|
|
149
|
+
"description": "Whirlpool 512-bit hash",
|
|
150
|
+
"example": "19fa61d75522a4669b44e39c1d2e1726c530232130d407f89afee0964997f7a73e83be698b288febcf88e3e03c4f0757ea8964e59b63d93708b138cc42a66eb3",
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"name": "LM Hash",
|
|
154
|
+
"regex": r"^[a-f0-9]{32}$",
|
|
155
|
+
"length": 32,
|
|
156
|
+
"description": "LAN Manager Hash (Windows legacy)",
|
|
157
|
+
"example": "e52cac67419a9a224a3b108f3fa6cb6d",
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
"name": "Tiger-192",
|
|
161
|
+
"regex": r"^[a-f0-9]{48}$",
|
|
162
|
+
"length": 48,
|
|
163
|
+
"description": "Tiger 192-bit",
|
|
164
|
+
"example": "9f00f599072300dd276abb38c8eb6dcae6f0f3b0ff6d0f29",
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
"name": "Haval-256",
|
|
168
|
+
"regex": r"^[a-f0-9]{64}$",
|
|
169
|
+
"length": 64,
|
|
170
|
+
"description": "Haval 256-bit",
|
|
171
|
+
"example": "4a665e3f9f31e52f42b08cde57a93b2cedc1b8a25a0a76a2a15abd76f5e0a48",
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"name": "Base64",
|
|
175
|
+
"regex": r"^[A-Za-z0-9+/]+=*$",
|
|
176
|
+
"length": None,
|
|
177
|
+
"description": "Base64 encoded string",
|
|
178
|
+
"example": "aGVsbG8gd29ybGQ=",
|
|
179
|
+
},
|
|
180
|
+
]
|
|
181
|
+
|
|
182
|
+
LENGTH_MAP = {
|
|
183
|
+
8: ["CRC32", "Adler32"],
|
|
184
|
+
32: ["MD5", "NTLM", "LM Hash"],
|
|
185
|
+
40: ["SHA-1", "RIPEMD-160"],
|
|
186
|
+
48: ["Tiger-192"],
|
|
187
|
+
56: ["SHA-224"],
|
|
188
|
+
64: ["SHA-256", "SHA3-256", "BLAKE2s", "Haval-256"],
|
|
189
|
+
96: ["SHA-384"],
|
|
190
|
+
128: ["SHA-512", "SHA3-512", "BLAKE2b", "Whirlpool"],
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def detect_hash(hash_string: str) -> list[dict]:
|
|
195
|
+
"""Return list of possible hash types for a given hash string."""
|
|
196
|
+
hash_string = hash_string.strip()
|
|
197
|
+
matches = []
|
|
198
|
+
|
|
199
|
+
for pattern in HASH_PATTERNS:
|
|
200
|
+
if re.match(pattern["regex"], hash_string, re.IGNORECASE):
|
|
201
|
+
matches.append(pattern)
|
|
202
|
+
|
|
203
|
+
# Deduplicate by name
|
|
204
|
+
seen = set()
|
|
205
|
+
unique = []
|
|
206
|
+
for m in matches:
|
|
207
|
+
if m["name"] not in seen:
|
|
208
|
+
seen.add(m["name"])
|
|
209
|
+
unique.append(m)
|
|
210
|
+
|
|
211
|
+
return unique
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def is_hex(s: str) -> bool:
|
|
215
|
+
try:
|
|
216
|
+
int(s, 16)
|
|
217
|
+
return True
|
|
218
|
+
except ValueError:
|
|
219
|
+
return False
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def classify_by_length(length: int) -> list[str]:
|
|
223
|
+
return LENGTH_MAP.get(length, [])
|
hashcrack/lookup.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import requests
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
HEADERS = {"User-Agent": "hashcrack/0.1.0 (CTF hash lookup tool)"}
|
|
6
|
+
TIMEOUT = 10
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def lookup_md5_online(hash_str: str) -> Optional[str]:
|
|
10
|
+
"""Try multiple free hash lookup APIs for MD5/SHA1/SHA256."""
|
|
11
|
+
hash_str = hash_str.lower().strip()
|
|
12
|
+
|
|
13
|
+
# md5decrypt.net
|
|
14
|
+
try:
|
|
15
|
+
url = f"https://md5decrypt.net/Api/api.php?hash={hash_str}&hash_type=md5&email=test@test.com&code=code1"
|
|
16
|
+
resp = requests.get(url, timeout=TIMEOUT, headers=HEADERS)
|
|
17
|
+
if resp.ok and resp.text and resp.text not in ("", "ACCESS_DENIED", "INVALID_HASH", "NOT_FOUND"):
|
|
18
|
+
return resp.text.strip()
|
|
19
|
+
except Exception:
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def lookup_nitrxgen(hash_str: str) -> Optional[str]:
|
|
26
|
+
"""Use nitrxgen rainbow table lookup for MD5."""
|
|
27
|
+
hash_str = hash_str.lower().strip()
|
|
28
|
+
try:
|
|
29
|
+
url = f"https://www.nitrxgen.net/md5db/{hash_str}"
|
|
30
|
+
resp = requests.get(url, timeout=TIMEOUT, headers=HEADERS)
|
|
31
|
+
if resp.ok and resp.text.strip():
|
|
32
|
+
return resp.text.strip()
|
|
33
|
+
except Exception:
|
|
34
|
+
pass
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def lookup_hashes_com(hash_str: str) -> Optional[str]:
|
|
39
|
+
"""Query hashes.com API."""
|
|
40
|
+
hash_str = hash_str.lower().strip()
|
|
41
|
+
try:
|
|
42
|
+
url = f"https://hashes.com/en/api/identify"
|
|
43
|
+
resp = requests.post(
|
|
44
|
+
url,
|
|
45
|
+
data={"hash": hash_str},
|
|
46
|
+
timeout=TIMEOUT,
|
|
47
|
+
headers=HEADERS,
|
|
48
|
+
)
|
|
49
|
+
if resp.ok:
|
|
50
|
+
data = resp.json()
|
|
51
|
+
if data.get("result"):
|
|
52
|
+
return data["result"]
|
|
53
|
+
except Exception:
|
|
54
|
+
pass
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def lookup_hashtoolkit(hash_str: str) -> Optional[str]:
|
|
59
|
+
"""Query hashtoolkit.com."""
|
|
60
|
+
hash_str = hash_str.lower().strip()
|
|
61
|
+
try:
|
|
62
|
+
url = f"https://hashtoolkit.com/reverse-hash/?hash={hash_str}"
|
|
63
|
+
resp = requests.get(url, timeout=TIMEOUT, headers=HEADERS)
|
|
64
|
+
if resp.ok:
|
|
65
|
+
import re
|
|
66
|
+
match = re.search(r'<span class="hashValue">(.*?)</span>', resp.text)
|
|
67
|
+
if match:
|
|
68
|
+
return match.group(1).strip()
|
|
69
|
+
except Exception:
|
|
70
|
+
pass
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def crackstation_lookup(hash_str: str) -> Optional[str]:
|
|
75
|
+
"""Query CrackStation via their API."""
|
|
76
|
+
hash_str = hash_str.lower().strip()
|
|
77
|
+
try:
|
|
78
|
+
url = "https://crackstation.net/crackstation-api/v1/hash.php"
|
|
79
|
+
resp = requests.post(
|
|
80
|
+
url,
|
|
81
|
+
data={"hash": hash_str, "submit": "Crack Hashes"},
|
|
82
|
+
timeout=TIMEOUT,
|
|
83
|
+
headers={**HEADERS, "Content-Type": "application/x-www-form-urlencoded"},
|
|
84
|
+
)
|
|
85
|
+
if resp.ok and resp.text:
|
|
86
|
+
import json
|
|
87
|
+
try:
|
|
88
|
+
results = json.loads(resp.text)
|
|
89
|
+
if isinstance(results, list) and results and results[0].get("cracked"):
|
|
90
|
+
return results[0].get("plaintext")
|
|
91
|
+
except Exception:
|
|
92
|
+
pass
|
|
93
|
+
except Exception:
|
|
94
|
+
pass
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def lookup_hash(hash_str: str, hash_type: Optional[str] = None) -> dict:
|
|
99
|
+
"""
|
|
100
|
+
Attempt to look up plaintext for a hash using multiple public services.
|
|
101
|
+
Returns dict with 'found', 'plaintext', 'source' keys.
|
|
102
|
+
"""
|
|
103
|
+
hash_str = hash_str.strip()
|
|
104
|
+
result = {"found": False, "plaintext": None, "source": None}
|
|
105
|
+
|
|
106
|
+
# Try multiple sources
|
|
107
|
+
sources = [
|
|
108
|
+
("nitrxgen (MD5 rainbow tables)", lookup_nitrxgen),
|
|
109
|
+
("hashes.com", lookup_hashes_com),
|
|
110
|
+
("hashtoolkit.com", lookup_hashtoolkit),
|
|
111
|
+
("crackstation.net", crackstation_lookup),
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
for source_name, fn in sources:
|
|
115
|
+
try:
|
|
116
|
+
plaintext = fn(hash_str)
|
|
117
|
+
if plaintext:
|
|
118
|
+
result["found"] = True
|
|
119
|
+
result["plaintext"] = plaintext
|
|
120
|
+
result["source"] = source_name
|
|
121
|
+
return result
|
|
122
|
+
except Exception:
|
|
123
|
+
continue
|
|
124
|
+
|
|
125
|
+
return result
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def generate_hash(text: str, algorithm: str) -> Optional[str]:
|
|
129
|
+
"""Generate hash of given text using specified algorithm."""
|
|
130
|
+
algorithm = algorithm.lower().replace("-", "").replace("_", "")
|
|
131
|
+
algo_map = {
|
|
132
|
+
"md5": "md5",
|
|
133
|
+
"sha1": "sha1",
|
|
134
|
+
"sha224": "sha224",
|
|
135
|
+
"sha256": "sha256",
|
|
136
|
+
"sha384": "sha384",
|
|
137
|
+
"sha512": "sha512",
|
|
138
|
+
"sha3224": "sha3_224",
|
|
139
|
+
"sha3256": "sha3_256",
|
|
140
|
+
"sha3384": "sha3_384",
|
|
141
|
+
"sha3512": "sha3_512",
|
|
142
|
+
"blake2b": "blake2b",
|
|
143
|
+
"blake2s": "blake2s",
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
algo_key = algo_map.get(algorithm)
|
|
147
|
+
if not algo_key:
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
h = hashlib.new(algo_key)
|
|
152
|
+
h.update(text.encode("utf-8"))
|
|
153
|
+
return h.hexdigest()
|
|
154
|
+
except ValueError:
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def verify_hash(hash_str: str, plaintext: str, algorithm: str) -> bool:
|
|
159
|
+
"""Verify that plaintext matches hash using given algorithm."""
|
|
160
|
+
computed = generate_hash(plaintext, algorithm)
|
|
161
|
+
if computed is None:
|
|
162
|
+
return False
|
|
163
|
+
return computed.lower() == hash_str.lower().strip()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 shazeus
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: hashcrack-cli
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Hash identification and lookup tool for CTF and security research
|
|
5
|
+
Author-email: shazeus <efeborazan07@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/shazeus/hashcrack
|
|
8
|
+
Project-URL: Repository, https://github.com/shazeus/hashcrack
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/shazeus/hashcrack/issues
|
|
10
|
+
Keywords: hash,ctf,security,md5,sha256,cracking,lookup,cli,hashing,cryptography
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Information Technology
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Security
|
|
22
|
+
Classifier: Topic :: Security :: Cryptography
|
|
23
|
+
Classifier: Topic :: Utilities
|
|
24
|
+
Requires-Python: >=3.9
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: click>=8.0
|
|
28
|
+
Requires-Dist: rich>=13.0
|
|
29
|
+
Requires-Dist: requests>=2.28
|
|
30
|
+
|
|
31
|
+
<p align="center">
|
|
32
|
+
<h1 align="center">hashcrack</h1>
|
|
33
|
+
<p align="center">Hash identification and lookup tool for CTF and security research.</p>
|
|
34
|
+
<p align="center">
|
|
35
|
+
<a href="https://pypi.org/project/hashcrack-cli/"><img src="https://img.shields.io/pypi/v/hashcrack-cli?color=blue&style=flat-square" alt="PyPI"></a>
|
|
36
|
+
<a href="https://pypi.org/project/hashcrack-cli/"><img src="https://img.shields.io/pypi/pyversions/hashcrack-cli?style=flat-square" alt="Python"></a>
|
|
37
|
+
<a href="https://github.com/shazeus/hashcrack/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="License"></a>
|
|
38
|
+
<a href="https://github.com/shazeus/hashcrack/stargazers"><img src="https://img.shields.io/github/stars/shazeus/hashcrack?style=flat-square" alt="Stars"></a>
|
|
39
|
+
</p>
|
|
40
|
+
</p>
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
**hashcrack** is a fast, feature-rich CLI tool for hash analysis in CTF competitions and security research. It auto-detects hash types (MD5, SHA-1, SHA-256, bcrypt, NTLM, and 20+ more), queries public hash databases, performs wordlist attacks, generates hashes, and provides deep statistical analysis — all with beautiful terminal output powered by Rich.
|
|
45
|
+
|
|
46
|
+
- **Auto-detection** — identifies 20+ hash types from MD5 to Argon2, bcrypt, NTLM, and scrypt
|
|
47
|
+
- **Online lookup** — queries multiple public hash cracking APIs simultaneously
|
|
48
|
+
- **Wordlist attack** — crack hashes locally using any wordlist (e.g. rockyou.txt)
|
|
49
|
+
- **Hash generation** — compute MD5, SHA-1, SHA-256, SHA-512, BLAKE2 and more in one command
|
|
50
|
+
- **Batch mode** — process hundreds of hashes from a file with optional auto-lookup
|
|
51
|
+
- **Verification** — confirm plaintext/hash pairs across all supported algorithms
|
|
52
|
+
- **Deep analysis** — entropy calculation, charset analysis, and type classification
|
|
53
|
+
- **JSON output** — every command supports `--json` for scripting and piping
|
|
54
|
+
|
|
55
|
+
## Installation
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install hashcrack-cli
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Or install from source:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
git clone https://github.com/shazeus/hashcrack
|
|
65
|
+
cd hashcrack
|
|
66
|
+
pip install -e .
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Usage
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# Identify a hash type
|
|
73
|
+
hashcrack identify 5d41402abc4b2a76b9719d911017c592
|
|
74
|
+
|
|
75
|
+
# Look up plaintext via public APIs
|
|
76
|
+
hashcrack lookup 5d41402abc4b2a76b9719d911017c592
|
|
77
|
+
|
|
78
|
+
# Generate hashes
|
|
79
|
+
hashcrack generate "hello world"
|
|
80
|
+
hashcrack generate "secret" --algo md5 --algo sha256
|
|
81
|
+
|
|
82
|
+
# Crack with a wordlist
|
|
83
|
+
hashcrack wordlist rockyou.txt 5d41402abc4b2a76b9719d911017c592
|
|
84
|
+
|
|
85
|
+
# Batch process hashes from a file
|
|
86
|
+
hashcrack batch hashes.txt --lookup
|
|
87
|
+
|
|
88
|
+
# Verify plaintext against hash
|
|
89
|
+
hashcrack verify 5d41402abc4b2a76b9719d911017c592 "hello"
|
|
90
|
+
|
|
91
|
+
# Deep analysis
|
|
92
|
+
hashcrack analyze 5d41402abc4b2a76b9719d911017c592
|
|
93
|
+
|
|
94
|
+
# List all supported hash types
|
|
95
|
+
hashcrack info
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Commands
|
|
99
|
+
|
|
100
|
+
| Command | Description |
|
|
101
|
+
|---------|-------------|
|
|
102
|
+
| `identify <hash>` | Auto-detect the type(s) of a hash |
|
|
103
|
+
| `lookup <hash>` | Query public databases for the plaintext |
|
|
104
|
+
| `generate <text>` | Generate hashes for a string (all algorithms or specific ones) |
|
|
105
|
+
| `wordlist <file> <hash>` | Crack a hash using a local wordlist |
|
|
106
|
+
| `batch <file>` | Process multiple hashes from a file |
|
|
107
|
+
| `verify <hash> <plaintext>` | Confirm a hash/plaintext pair |
|
|
108
|
+
| `analyze <hash>` | Full analysis: type + lookup + entropy + charset |
|
|
109
|
+
| `info [type]` | Show details for a hash type, or list all supported types |
|
|
110
|
+
|
|
111
|
+
## Configuration
|
|
112
|
+
|
|
113
|
+
All commands support these global options:
|
|
114
|
+
|
|
115
|
+
| Flag | Description |
|
|
116
|
+
|------|-------------|
|
|
117
|
+
| `--json` | Output results as JSON for scripting |
|
|
118
|
+
| `-q / --quiet` | Minimal output (useful in scripts) |
|
|
119
|
+
| `-h / --help` | Show help for any command |
|
|
120
|
+
| `-V / --version` | Show version |
|
|
121
|
+
|
|
122
|
+
## Supported Hash Types
|
|
123
|
+
|
|
124
|
+
MD5, SHA-1, SHA-224, SHA-256, SHA-384, SHA-512, SHA3-256, SHA3-512, NTLM, LM Hash, bcrypt, Argon2, scrypt, MD5 Crypt, SHA-512 Crypt, RIPEMD-160, BLAKE2b, BLAKE2s, CRC32, Adler32, Tiger-192, Haval-256, MySQL4/5, Whirlpool, Base64
|
|
125
|
+
|
|
126
|
+
## License
|
|
127
|
+
|
|
128
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
hashcrack/__init__.py,sha256=j8CLnPWX6MTBo9iUD2tq6PeAtuutXV9oYyiLl8TSHRA,45
|
|
2
|
+
hashcrack/__main__.py,sha256=2tXjfx7WOXfrjrcywu-MAx-ievt3WsEY9ARdB3Jhh5w,68
|
|
3
|
+
hashcrack/cli.py,sha256=SgN5lZ6WKPK96DH5IPLFryQ1Vgno2rRHz74w-EHcQrU,24980
|
|
4
|
+
hashcrack/detector.py,sha256=IFAWVV7WQXwYsrCYoT9o2dny8cbznu4nWrdcSWHlBOU,6947
|
|
5
|
+
hashcrack/lookup.py,sha256=wzSiuNA82va7gB_XUnI6gsQzurQxO4_txXjy3--A4tU,4998
|
|
6
|
+
hashcrack_cli-0.1.1.dist-info/LICENSE,sha256=HZHbJWXuWLEERO18ylRPsLaCFaWaJmc56UdeAaLJ7E8,1064
|
|
7
|
+
hashcrack_cli-0.1.1.dist-info/METADATA,sha256=v8R8USdTnaBVY9ZehNezdC9u6VhOgTUfZWH7y6f_O6Q,5128
|
|
8
|
+
hashcrack_cli-0.1.1.dist-info/WHEEL,sha256=beeZ86-EfXScwlR_HKu4SllMC9wUEj_8Z_4FJ3egI2w,91
|
|
9
|
+
hashcrack_cli-0.1.1.dist-info/entry_points.txt,sha256=Zhe4EhOuzYtMRXCwitA5F5avKfRYPBjPZ60O99JhVpE,48
|
|
10
|
+
hashcrack_cli-0.1.1.dist-info/top_level.txt,sha256=WAtfAMG7bU80JoUtZJU_12yeJC3a66sVKJEzM9di1xg,10
|
|
11
|
+
hashcrack_cli-0.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
hashcrack
|