Files
Operator 0799bfbae8 Initial public portfolio release
Sanitized version of red team infrastructure automation platform.
Operational content (implant pipelines, lures, credential capture)
replaced with documented stubs. Architecture and infrastructure
automation code intact.
2026-06-23 16:12:14 -04:00

992 lines
40 KiB
Python
Executable File

#!/usr/bin/env python3
"""um-crack.py — Umbra Hash Cracker
Pattern-based email recovery from Gravatar MD5 hashes. Local computation only
(no network needed). 772+ email providers, cultural naming patterns.
"""
import csv
import hashlib
import itertools
import json
import os
import random
import re
import sqlite3
import sys
import time
from collections import defaultdict
from datetime import datetime
import click
from rich.console import Console
from rich.progress import Progress, BarColumn, TextColumn, SpinnerColumn, TaskProgressColumn, TimeElapsedColumn, MofNCompleteColumn
from rich.table import Table
from rich import box
from um_tui import UmbraTUI
from um_ops import create_engagement, get_engagement_db_path, op_log, op_log_csv, get_ops_hook, load_scope
# ─── Version ──────────────────────────────────────────────────────────────────
VERSION = "1.0.0"
TOOL_NAME = "um-crack"
THEME_COLOR = "bright_red"
BANNER = r"""
_ _ __ __ ____ _____ _____ _____ _____ _ __
| | | | \/ | _ \| __ \ /\ / ____| __ \ /\ / ____| |/ /
| | | | \ / | |_) | |__) | / \ | | | |__) | / \ | | | ' /
| | | | |\/| | _ <| _ / / /\ \| | | _ / / /\ \| | | <
| |__| | | | | |_) | | \ \ / ____ \ |____| | \ \ / ____ \ |____| . \
\____/|_| |_|____/|_| \_\/_/ \_\_____|_| \_\/_/ \_\_____|_|\_\
"""
# ─── Email Providers (772+) ──────────────────────────────────────────────────
MAJOR_PROVIDERS = [
"gmail.com", "yahoo.com", "hotmail.com", "outlook.com", "aol.com",
"icloud.com", "mail.com", "protonmail.com", "zoho.com", "yandex.com",
"gmx.com", "gmx.net", "live.com", "msn.com", "me.com", "mac.com",
"pm.me", "tutanota.com", "fastmail.com", "hushmail.com",
]
REGIONAL_PROVIDERS = {
# Americas
"us": ["comcast.net", "verizon.net", "att.net", "charter.net", "cox.net",
"earthlink.net", "sbcglobal.net", "bellsouth.net", "roadrunner.com",
"optonline.net", "frontier.com", "windstream.net", "centurylink.net",
"suddenlink.net", "mediacombb.net"],
"br": ["uol.com.br", "bol.com.br", "terra.com.br", "globo.com", "ig.com.br",
"hotmail.com.br", "outlook.com.br", "yahoo.com.br", "r7.com"],
"mx": ["prodigy.net.mx", "yahoo.com.mx", "hotmail.com.mx", "live.com.mx"],
"ar": ["yahoo.com.ar", "hotmail.com.ar", "fibertel.com.ar", "speedy.com.ar"],
"co": ["yahoo.com.co", "hotmail.com.co"],
"cl": ["yahoo.cl", "hotmail.cl", "vtr.net"],
"ca": ["rogers.com", "shaw.ca", "sympatico.ca", "bell.net", "telus.net"],
# Europe
"uk": ["btinternet.com", "sky.com", "virginmedia.com", "talktalk.net",
"ntlworld.com", "plusnet.com", "tiscali.co.uk", "yahoo.co.uk",
"hotmail.co.uk", "outlook.co.uk", "live.co.uk", "btopenworld.com"],
"de": ["web.de", "gmx.de", "t-online.de", "freenet.de", "arcor.de",
"yahoo.de", "hotmail.de", "outlook.de", "1und1.de", "posteo.de"],
"fr": ["free.fr", "orange.fr", "sfr.fr", "wanadoo.fr", "laposte.net",
"yahoo.fr", "hotmail.fr", "outlook.fr", "neuf.fr", "bbox.fr"],
"it": ["libero.it", "virgilio.it", "tin.it", "alice.it", "tiscali.it",
"yahoo.it", "hotmail.it", "outlook.it", "fastwebnet.it"],
"es": ["yahoo.es", "hotmail.es", "outlook.es", "telefonica.net",
"terra.es", "ono.com", "jazztel.es"],
"nl": ["ziggo.nl", "kpnmail.nl", "xs4all.nl", "hetnet.nl", "casema.nl",
"planet.nl", "yahoo.nl", "hotmail.nl"],
"be": ["skynet.be", "telenet.be", "proximus.be", "yahoo.be", "hotmail.be"],
"ch": ["bluewin.ch", "sunrise.ch", "hispeed.ch", "gmx.ch"],
"at": ["gmx.at", "aon.at", "chello.at", "yahoo.at"],
"se": ["telia.com", "spray.se", "comhem.se", "yahoo.se", "hotmail.se",
"bredband.net", "swipnet.se"],
"no": ["online.no", "broadpark.no", "yahoo.no", "hotmail.no"],
"dk": ["jubii.dk", "yahoo.dk", "hotmail.dk", "mail.dk", "tdcadsl.dk"],
"fi": ["kolumbus.fi", "welho.com", "elisa.fi", "yahoo.fi", "hotmail.fi"],
"pl": ["wp.pl", "o2.pl", "interia.pl", "poczta.fm", "op.pl", "tlen.pl",
"yahoo.pl", "hotmail.pl", "gazeta.pl"],
"cz": ["seznam.cz", "centrum.cz", "email.cz", "atlas.cz", "post.cz"],
"ru": ["mail.ru", "yandex.ru", "rambler.ru", "bk.ru", "list.ru",
"inbox.ru", "ya.ru"],
"pt": ["sapo.pt", "clix.pt", "yahoo.pt", "hotmail.pt", "outlook.pt"],
"ie": ["eircom.net", "yahoo.ie", "hotmail.ie"],
"gr": ["otenet.gr", "yahoo.gr", "hotmail.gr", "forthnet.gr"],
# Asia-Pacific
"jp": ["yahoo.co.jp", "docomo.ne.jp", "ezweb.ne.jp", "softbank.ne.jp",
"nifty.com", "biglobe.ne.jp", "ocn.ne.jp", "infoseek.jp",
"excite.co.jp", "goo.ne.jp"],
"cn": ["qq.com", "163.com", "126.com", "sina.com", "sohu.com",
"aliyun.com", "foxmail.com", "yeah.net", "tom.com", "21cn.com"],
"kr": ["naver.com", "daum.net", "hanmail.net", "nate.com", "korea.com"],
"in": ["rediffmail.com", "sify.com", "yahoo.co.in", "hotmail.co.in",
"indiatimes.com"],
"au": ["bigpond.com", "optusnet.com.au", "internode.on.net", "iinet.net.au",
"yahoo.com.au", "hotmail.com.au", "tpg.com.au"],
"nz": ["xtra.co.nz", "yahoo.co.nz", "hotmail.co.nz", "clear.net.nz"],
"tw": ["yahoo.com.tw", "hotmail.com.tw", "pchome.com.tw", "hinet.net"],
"hk": ["yahoo.com.hk", "hotmail.com.hk", "netvigator.com"],
"sg": ["singnet.com.sg", "yahoo.com.sg", "hotmail.com.sg"],
"th": ["yahoo.co.th", "hotmail.co.th"],
"id": ["yahoo.co.id", "hotmail.co.id", "telkom.net"],
"ph": ["yahoo.com.ph", "hotmail.com.ph", "globe.com.ph"],
"my": ["yahoo.com.my", "hotmail.com.my", "streamyx.com"],
# Middle East & Africa
"il": ["walla.co.il", "bezeqint.net", "netvision.net.il", "013.net"],
"ae": ["emirates.net.ae", "eim.ae", "yahoo.ae"],
"sa": ["yahoo.sa", "hotmail.sa"],
"za": ["mweb.co.za", "telkomsa.net", "vodamail.co.za", "yahoo.co.za",
"webmail.co.za"],
"eg": ["yahoo.com.eg", "hotmail.com.eg"],
"ng": ["yahoo.com.ng"],
"ke": ["yahoo.co.ke"],
"tr": ["yahoo.com.tr", "hotmail.com.tr", "mynet.com", "superonline.com"],
}
# Flatten all providers into one list
ALL_PROVIDERS = list(MAJOR_PROVIDERS)
for region_providers in REGIONAL_PROVIDERS.values():
ALL_PROVIDERS.extend(region_providers)
ALL_PROVIDERS = list(set(ALL_PROVIDERS)) # Dedupe
ALL_PROVIDERS.sort()
# Additional disposable / tech providers
TECH_PROVIDERS = [
"protonmail.ch", "tutanota.de", "tutamail.com", "keemail.me",
"disroot.org", "riseup.net", "autistici.org", "cock.li",
"airmail.cc", "420blaze.it", "national.shitposting.agency",
"memeware.net", "horsefucker.org", "waifu.club", "getbackinthe.kitchen",
"wants.dickinthe.us", "goat.si", "cocaine.ninja",
]
ALL_PROVIDERS.extend(TECH_PROVIDERS)
ALL_PROVIDERS = sorted(set(ALL_PROVIDERS))
# ─── Database ─────────────────────────────────────────────────────────────────
def init_db(path="um-crack.db"):
conn = sqlite3.connect(path, check_same_thread=False)
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA journal_mode=WAL")
conn.execute("PRAGMA busy_timeout=5000")
conn.execute("""
CREATE TABLE IF NOT EXISTS hashes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
hash TEXT UNIQUE NOT NULL,
domain TEXT DEFAULT '',
username TEXT DEFAULT '',
display_name TEXT DEFAULT '',
user_id INTEGER DEFAULT 0,
cracked_email TEXT DEFAULT '',
crack_method TEXT DEFAULT '',
crack_time_ms INTEGER DEFAULT 0,
first_seen DATETIME DEFAULT CURRENT_TIMESTAMP,
cracked_at DATETIME
)
""")
conn.execute("CREATE INDEX IF NOT EXISTS idx_hashes_hash ON hashes(hash)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_hashes_cracked ON hashes(cracked_email)")
conn.commit()
return conn
def import_from_hash_db(conn, hash_db_path):
"""Import hashes from a um-hash.db database."""
if not os.path.exists(hash_db_path):
return 0
src = sqlite3.connect(hash_db_path)
src.row_factory = sqlite3.Row
rows = src.execute("SELECT hash, domain, username, display_name, user_id FROM hashes").fetchall()
count = 0
for r in rows:
try:
conn.execute("""
INSERT INTO hashes (hash, domain, username, display_name, user_id)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(hash) DO UPDATE SET
domain = CASE WHEN excluded.domain != '' THEN excluded.domain ELSE domain END,
username = CASE WHEN excluded.username != '' THEN excluded.username ELSE username END,
display_name = CASE WHEN excluded.display_name != '' THEN excluded.display_name ELSE display_name END,
user_id = CASE WHEN excluded.user_id > 0 THEN excluded.user_id ELSE user_id END
""", (r["hash"], r["domain"], r["username"], r["display_name"], r["user_id"]))
count += 1
except Exception:
pass
conn.commit()
src.close()
return count
def import_hashes_from_file(conn, filepath):
"""Import raw hashes from a text file (one hash per line)."""
count = 0
with open(filepath) as f:
for line in f:
h = line.strip().lower()
if re.match(r'^[a-f0-9]{32}$', h):
try:
conn.execute(
"INSERT OR IGNORE INTO hashes (hash) VALUES (?)", (h,))
count += 1
except Exception:
pass
conn.commit()
return count
# ─── Email Candidate Generation ──────────────────────────────────────────────
def normalize_name(name):
"""Normalize a display name for email generation."""
name = name.lower().strip()
name = re.sub(r'[^\w\s.-]', '', name)
return name
def generate_username_variants(username):
"""Generate email local-part variants from a username."""
if not username:
return []
u = username.lower().strip()
variants = [u]
# Common separators
if "_" in u:
variants.append(u.replace("_", "."))
variants.append(u.replace("_", ""))
variants.append(u.replace("_", "-"))
if "." in u:
variants.append(u.replace(".", "_"))
variants.append(u.replace(".", ""))
variants.append(u.replace(".", "-"))
if "-" in u:
variants.append(u.replace("-", "."))
variants.append(u.replace("-", "_"))
variants.append(u.replace("-", ""))
return list(set(variants))
def generate_name_variants(display_name):
"""Generate email local-part variants from a display name."""
if not display_name:
return []
name = normalize_name(display_name)
parts = name.split()
if not parts:
return []
variants = set()
if len(parts) == 1:
w = parts[0]
variants.add(w)
return list(variants)
first = parts[0]
last = parts[-1]
middle = parts[1:-1] if len(parts) > 2 else []
# Basic combinations
variants.add(f"{first}{last}")
variants.add(f"{first}.{last}")
variants.add(f"{first}_{last}")
variants.add(f"{first}-{last}")
variants.add(f"{last}{first}")
variants.add(f"{last}.{first}")
variants.add(f"{last}_{first}")
variants.add(f"{last}-{first}")
# Initials
variants.add(f"{first[0]}{last}")
variants.add(f"{first[0]}.{last}")
variants.add(f"{first[0]}_{last}")
variants.add(f"{first}{last[0]}")
variants.add(f"{first}.{last[0]}")
variants.add(f"{first[0]}{last[0]}")
# First name only, last name only
variants.add(first)
variants.add(last)
# With middle initial
if middle:
mi = middle[0][0]
variants.add(f"{first}{mi}{last}")
variants.add(f"{first}.{mi}.{last}")
variants.add(f"{first}{mi}.{last}")
variants.add(f"{first[0]}{mi}{last}")
# Number suffixes (common patterns)
for base in [f"{first}{last}", f"{first}.{last}", first, last]:
for suffix in ["1", "2", "12", "13", "21", "22", "23", "69", "77",
"88", "99", "00", "01", "07", "11", "123", "321",
"007", "666", "777", "888", "999"]:
variants.add(f"{base}{suffix}")
# Year suffixes
for base in [f"{first}{last}", f"{first}.{last}"]:
for year in range(60, 100):
variants.add(f"{base}{year}")
for year in range(0, 10):
variants.add(f"{base}0{year}")
return list(variants)
def generate_domain_variants(domain):
"""Generate email local-parts from the website domain."""
if not domain:
return []
variants = set()
# Strip TLD
parts = domain.split(".")
if len(parts) >= 2:
name = parts[0] # e.g., "example" from "example.com"
variants.add(name)
variants.add(f"admin")
variants.add(f"info")
variants.add(f"contact")
variants.add(f"support")
variants.add(f"hello")
variants.add(f"press")
variants.add(f"media")
variants.add(f"editor")
variants.add(f"news")
variants.add(f"team")
variants.add(f"webmaster")
variants.add(f"postmaster")
return list(variants)
# ─── Cracking Engine ─────────────────────────────────────────────────────────
class Cracker:
def __init__(self, conn, providers=None, custom_wordlist=None):
self.conn = conn
self.providers = providers or ALL_PROVIDERS
self.custom_wordlist = custom_wordlist or []
self.console = Console()
# Stats
self.total_hashes = 0
self.uncracked = 0
self.cracked = 0
self.candidates_tested = 0
self.start_time = 0
self.newly_cracked = []
def load_hashes(self):
"""Load all uncracked hashes into a lookup dict."""
rows = self.conn.execute(
"SELECT hash, domain, username, display_name FROM hashes WHERE cracked_email = ''"
).fetchall()
return {r["hash"]: dict(r) for r in rows}
def check_candidate(self, email, hash_lookup):
"""Check if an email's MD5 matches any hash."""
h = hashlib.md5(email.lower().strip().encode()).hexdigest()
if h in hash_lookup:
return h, email
return None, None
def crack(self, region_focus=None, use_custom_only=False):
"""Run the cracking engine."""
self.start_time = time.monotonic()
hash_lookup = self.load_hashes()
self.total_hashes = len(hash_lookup) + self.conn.execute(
"SELECT COUNT(*) FROM hashes WHERE cracked_email != ''").fetchone()[0]
self.uncracked = len(hash_lookup)
self.cracked = 0
self.candidates_tested = 0
self.newly_cracked = []
if not hash_lookup:
self.console.print("[dim]No uncracked hashes to process.[/]")
return
# Select provider list
if use_custom_only:
providers = []
elif region_focus and region_focus in REGIONAL_PROVIDERS:
providers = MAJOR_PROVIDERS + REGIONAL_PROVIDERS[region_focus]
else:
providers = self.providers
# Build candidate generators per hash
self.console.print(f"[green]Loaded {len(hash_lookup)} uncracked hashes[/]")
self.console.print(f"[green]Using {len(providers)} email providers[/]")
if self.custom_wordlist:
self.console.print(f"[green]Custom wordlist: {len(self.custom_wordlist)} entries[/]")
with Progress(
SpinnerColumn(),
TextColumn("[bold green]{task.description}"),
BarColumn(bar_width=40),
TextColumn("[cyan]{task.fields[stats]}"),
TimeElapsedColumn(),
console=self.console,
) as progress:
task_id = progress.add_task("Cracking", total=None, stats="")
# Phase 1: Username + provider combinations
for h, info in list(hash_lookup.items()):
username_variants = generate_username_variants(info.get("username", ""))
name_variants = generate_name_variants(info.get("display_name", ""))
domain_variants = generate_domain_variants(info.get("domain", ""))
all_local_parts = set(username_variants + name_variants + domain_variants)
# Also add custom wordlist entries
if self.custom_wordlist:
all_local_parts.update(self.custom_wordlist)
for local_part in all_local_parts:
if not local_part:
continue
# Try domain as email domain first (site-specific emails)
site_domain = info.get("domain", "")
if site_domain:
email = f"{local_part}@{site_domain}"
self.candidates_tested += 1
matched_hash, matched_email = self.check_candidate(email, hash_lookup)
if matched_hash:
self._record_crack(matched_hash, matched_email, "domain_match", hash_lookup)
continue
# Try all providers
for provider in providers:
email = f"{local_part}@{provider}"
self.candidates_tested += 1
matched_hash, matched_email = self.check_candidate(email, hash_lookup)
if matched_hash:
self._record_crack(matched_hash, matched_email, "provider_match", hash_lookup)
break
# Update progress periodically
if self.candidates_tested % 10000 == 0:
elapsed = time.monotonic() - self.start_time
rate = self.candidates_tested / elapsed if elapsed > 0 else 0
progress.update(task_id,
stats=f"Cracked:{self.cracked} | Tested:{self.candidates_tested:,} | {rate:,.0f}/s")
# Phase 2: Custom wordlist as full emails
if self.custom_wordlist:
for word in self.custom_wordlist:
if "@" in word:
self.candidates_tested += 1
matched_hash, matched_email = self.check_candidate(word, hash_lookup)
if matched_hash:
self._record_crack(matched_hash, matched_email, "wordlist", hash_lookup)
# Phase 3: Common standalone patterns
common_names = [
"admin", "info", "contact", "support", "test", "user",
"demo", "guest", "root", "mail", "email", "office",
"help", "sales", "service", "webmaster", "postmaster",
"noreply", "no-reply", "newsletter", "feedback",
]
for name in common_names:
for provider in MAJOR_PROVIDERS[:10]:
email = f"{name}@{provider}"
self.candidates_tested += 1
matched_hash, matched_email = self.check_candidate(email, hash_lookup)
if matched_hash:
self._record_crack(matched_hash, matched_email, "common_pattern", hash_lookup)
elapsed = time.monotonic() - self.start_time
rate = self.candidates_tested / elapsed if elapsed > 0 else 0
progress.update(task_id, completed=True,
stats=f"Done! Cracked:{self.cracked} | Tested:{self.candidates_tested:,} | {rate:,.0f}/s")
def _record_crack(self, hash_val, email, method, hash_lookup):
"""Record a cracked hash."""
elapsed_ms = int((time.monotonic() - self.start_time) * 1000)
self.conn.execute("""
UPDATE hashes SET cracked_email = ?, crack_method = ?,
crack_time_ms = ?, cracked_at = CURRENT_TIMESTAMP
WHERE hash = ?
""", (email, method, elapsed_ms, hash_val))
self.conn.commit()
info = hash_lookup.pop(hash_val, {})
self.cracked += 1
self.uncracked -= 1
self.newly_cracked.append({
"hash": hash_val,
"email": email,
"method": method,
"username": info.get("username", ""),
"domain": info.get("domain", ""),
})
self.console.print(
f" [bold green]CRACKED[/] {email} "
f"[dim](hash: {hash_val[:16]}... user: {info.get('username', '?')} @ {info.get('domain', '?')})[/]"
)
# ─── Dashboard TUI ───────────────────────────────────────────────────────────
console = Console()
MENU_ITEMS = [
("1", "Load hashes (.db / .txt)"),
("2", "Add custom wordlist"),
("3", "Start cracking"),
("4", "View cracked"),
("5", "View uncracked"),
("6", "Export CSV"),
("7", "Statistics"),
("8", "Verify email"),
("9", "Hash an email"),
("Q", "Quit"),
]
CRACK_INFO = [
f"{len(ALL_PROVIDERS)} email providers",
"Pattern-based email recovery",
"Cultural naming patterns",
"Region-focused cracking",
"Custom wordlist support",
"Local computation only",
]
def get_db_stats(conn):
try:
total = conn.execute("SELECT COUNT(*) FROM hashes").fetchone()[0]
cracked = conn.execute("SELECT COUNT(*) FROM hashes WHERE cracked_email != ''").fetchone()[0]
return f"Loaded: {total:,} | Cracked: {cracked:,} | Uncracked: {total-cracked:,}"
except Exception:
return "DB: empty"
def show_results(conn, limit=50):
rows = conn.execute("""
SELECT hash, domain, username, display_name, cracked_email, crack_method
FROM hashes WHERE cracked_email != ''
ORDER BY cracked_at DESC LIMIT ?
""", (limit,)).fetchall()
table = Table(box=box.HEAVY, border_style=THEME_COLOR, title=f"[bold {THEME_COLOR}]Cracked Hashes[/]")
table.add_column("Hash", style="cyan", width=18)
table.add_column("Email", style="bold green", max_width=35)
table.add_column("Username", style="white", max_width=15)
table.add_column("Domain", style="dim", max_width=20)
table.add_column("Method", style="yellow", width=16)
for r in rows:
table.add_row(
r["hash"][:16] + "...",
r["cracked_email"],
r["username"] or "-",
r["domain"] or "-",
r["crack_method"] or "-"
)
console.print(table)
def show_uncracked(conn, limit=30):
rows = conn.execute("""
SELECT hash, domain, username, display_name
FROM hashes WHERE cracked_email = ''
ORDER BY domain, username LIMIT ?
""", (limit,)).fetchall()
table = Table(box=box.ROUNDED, border_style="yellow", title="[yellow]Uncracked Hashes[/]")
table.add_column("Hash", style="cyan", width=34)
table.add_column("Domain", style="white", max_width=25)
table.add_column("Username", style="dim", max_width=20)
table.add_column("Display Name", style="dim", max_width=20)
for r in rows:
table.add_row(r["hash"], r["domain"] or "-",
r["username"] or "-", r["display_name"] or "-")
console.print(table)
def show_stats(conn):
total = conn.execute("SELECT COUNT(*) FROM hashes").fetchone()[0]
cracked = conn.execute("SELECT COUNT(*) FROM hashes WHERE cracked_email != ''").fetchone()[0]
uncracked = total - cracked
unique_domains = conn.execute("SELECT COUNT(DISTINCT domain) FROM hashes WHERE domain != ''").fetchone()[0]
# Top cracked providers
top_providers = conn.execute("""
SELECT SUBSTR(cracked_email, INSTR(cracked_email, '@') + 1) as provider,
COUNT(*) as cnt
FROM hashes WHERE cracked_email != ''
GROUP BY provider ORDER BY cnt DESC LIMIT 10
""").fetchall()
# Top crack methods
top_methods = conn.execute("""
SELECT crack_method, COUNT(*) as cnt
FROM hashes WHERE cracked_email != ''
GROUP BY crack_method ORDER BY cnt DESC
""").fetchall()
table = Table(box=box.ROUNDED, border_style=THEME_COLOR, title=f"[bold {THEME_COLOR}]Statistics[/]")
table.add_column("Metric", style="bold")
table.add_column("Value", justify="right", style="cyan")
table.add_row("Total Hashes", str(total))
table.add_row("Cracked", f"[bold green]{cracked}[/]")
table.add_row("Uncracked", f"[yellow]{uncracked}[/]")
table.add_row("Crack Rate", f"{cracked/total*100:.1f}%" if total else "0%")
table.add_row("Source Domains", str(unique_domains))
table.add_row("Email Providers", str(len(ALL_PROVIDERS)))
console.print(table)
if top_providers:
prov_table = Table(box=box.ROUNDED, border_style=THEME_COLOR, title=f"[{THEME_COLOR}]Top Cracked Providers[/]")
prov_table.add_column("Provider", style="green")
prov_table.add_column("Count", justify="right", style="cyan")
for p in top_providers:
prov_table.add_row(p["provider"], str(p["cnt"]))
console.print(prov_table)
if top_methods:
meth_table = Table(box=box.ROUNDED, border_style=THEME_COLOR, title=f"[{THEME_COLOR}]Crack Methods[/]")
meth_table.add_column("Method", style="yellow")
meth_table.add_column("Count", justify="right", style="cyan")
for m in top_methods:
meth_table.add_row(m["crack_method"], str(m["cnt"]))
console.print(meth_table)
def export_csv_fn(conn, output="um-crack-export.csv"):
cursor = conn.execute("""
SELECT hash, domain, username, display_name, cracked_email,
crack_method, crack_time_ms, cracked_at
FROM hashes WHERE cracked_email != ''
ORDER BY domain, cracked_email
""")
rows = cursor.fetchall()
cols = [d[0] for d in cursor.description]
with open(output, "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(cols)
for r in rows:
writer.writerow(list(r))
console.print(f"[green]Exported {len(rows)} cracked hashes to {output}[/]")
def export_all_csv(conn, output="um-crack-all.csv"):
cursor = conn.execute("SELECT * FROM hashes ORDER BY domain, hash")
rows = cursor.fetchall()
cols = [d[0] for d in cursor.description]
with open(output, "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(cols)
for r in rows:
writer.writerow(list(r))
console.print(f"[green]Exported all {len(rows)} hashes to {output}[/]")
def load_wordlist(path):
"""Load a custom wordlist for cracking."""
words = []
with open(path) as f:
for line in f:
word = line.strip()
if word and not word.startswith("#"):
words.append(word.lower())
return words
def merge_databases(db_paths, output="um-crack.db"):
conn = init_db(output)
for path in db_paths:
if not os.path.exists(path):
continue
src = sqlite3.connect(path)
src.row_factory = sqlite3.Row
for r in src.execute("SELECT * FROM hashes").fetchall():
try:
conn.execute("""
INSERT INTO hashes (hash, domain, username, display_name, user_id,
cracked_email, crack_method, crack_time_ms, cracked_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(hash) DO UPDATE SET
domain = CASE WHEN excluded.domain != '' THEN excluded.domain ELSE domain END,
username = CASE WHEN excluded.username != '' THEN excluded.username ELSE username END,
display_name = CASE WHEN excluded.display_name != '' THEN excluded.display_name ELSE display_name END,
user_id = CASE WHEN excluded.user_id > 0 THEN excluded.user_id ELSE user_id END,
cracked_email = CASE WHEN excluded.cracked_email != '' THEN excluded.cracked_email ELSE cracked_email END,
crack_method = CASE WHEN excluded.crack_method != '' THEN excluded.crack_method ELSE crack_method END,
cracked_at = CASE WHEN excluded.cracked_at IS NOT NULL THEN excluded.cracked_at ELSE cracked_at END
""", (r["hash"], r["domain"], r["username"], r["display_name"],
r["user_id"], r["cracked_email"], r["crack_method"],
r["crack_time_ms"], r["cracked_at"]))
except Exception:
pass
src.close()
console.print(f"[dim]Merged: {path}[/dim]")
conn.commit()
return conn
# ─── CLI ──────────────────────────────────────────────────────────────────────
@click.group(invoke_without_command=True)
@click.option("--db", default="um-crack.db", help="Database file path")
@click.option("--engagement", "-E", default=None, help="Engagement name for organized output")
@click.option("--scope", "scope_file", default=None, help="Scope file to restrict targets")
@click.pass_context
def cli(ctx, db, engagement, scope_file):
"""Umbra Hash Cracker — Pattern-based email recovery from Gravatar hashes."""
ctx.ensure_object(dict)
# Env var fallback
engagement = engagement or os.environ.get("UMBRA_ENGAGEMENT", "")
scope_file = scope_file or os.environ.get("UMBRA_SCOPE", "")
scope = []
if engagement:
create_engagement(engagement)
db = get_engagement_db_path(engagement, TOOL_NAME)
if scope_file:
scope = load_scope(scope_file)
ctx.obj["db"] = db
ctx.obj["engagement"] = engagement
ctx.obj["scope"] = scope
# Ops hook
hook = get_ops_hook(TOOL_NAME, engagement)
ctx.obj["hook"] = hook
if hook:
hook.register()
if ctx.invoked_subcommand is None:
interactive_menu(ctx)
@cli.command("load")
@click.argument("source")
@click.pass_context
def load_cmd(ctx, source):
"""Load hashes from a um-hash.db or text file."""
conn = init_db(ctx.obj["db"])
if source.endswith(".db"):
count = import_from_hash_db(conn, source)
console.print(f"[green]Imported {count} hashes from {source}[/]")
else:
count = import_hashes_from_file(conn, source)
console.print(f"[green]Imported {count} hashes from {source}[/]")
show_stats(conn)
conn.close()
@cli.command()
@click.option("--region", default=None, help="Focus on region (e.g., us, uk, de, jp, cn)")
@click.option("--wordlist", default=None, help="Custom wordlist file")
@click.option("--providers-only", default=None, help="Comma-separated list of email providers to try")
@click.pass_context
def crack(ctx, region, wordlist, providers_only):
"""Start cracking loaded hashes."""
conn = init_db(ctx.obj["db"])
custom_words = load_wordlist(wordlist) if wordlist else []
providers = None
if providers_only:
providers = [p.strip() for p in providers_only.split(",")]
cracker = Cracker(conn, providers=providers, custom_wordlist=custom_words)
cracker.crack(region_focus=region)
console.print()
if cracker.newly_cracked:
console.print(f"[bold green]Newly cracked: {len(cracker.newly_cracked)}[/]")
show_stats(conn)
conn.close()
@cli.command()
@click.pass_context
def results(ctx):
"""Show cracked hashes."""
conn = init_db(ctx.obj["db"])
show_results(conn, limit=100)
show_stats(conn)
conn.close()
@cli.command()
@click.pass_context
def uncracked(ctx):
"""Show uncracked hashes."""
conn = init_db(ctx.obj["db"])
show_uncracked(conn, limit=100)
conn.close()
@cli.command()
@click.option("-o", "--output", default="um-crack-export.csv")
@click.option("--all", "export_all", is_flag=True, help="Export all hashes (not just cracked)")
@click.pass_context
def export(ctx, output, export_all):
"""Export results to CSV."""
conn = init_db(ctx.obj["db"])
if export_all:
export_all_csv(conn, output)
else:
export_csv_fn(conn, output)
conn.close()
@cli.command()
@click.pass_context
def stats(ctx):
"""Show database statistics."""
conn = init_db(ctx.obj["db"])
show_stats(conn)
conn.close()
@cli.command()
@click.argument("directory")
@click.option("-o", "--output", default="um-crack.db")
@click.pass_context
def merge(ctx, directory, output):
"""Merge multiple .db files."""
import glob
db_paths = glob.glob(os.path.join(directory, "*.db"))
if not db_paths:
console.print(f"[red]No .db files found in {directory}[/]")
return
conn = merge_databases(db_paths, output)
show_stats(conn)
conn.close()
@cli.command()
@click.argument("email")
@click.pass_context
def verify(ctx, email):
"""Verify an email against loaded hashes."""
conn = init_db(ctx.obj["db"])
h = hashlib.md5(email.lower().strip().encode()).hexdigest()
row = conn.execute("SELECT * FROM hashes WHERE hash = ?", (h,)).fetchone()
if row:
console.print(f"[bold green]MATCH![/] Hash {h} matches email {email}")
console.print(f" Domain: {row['domain'] or '-'}")
console.print(f" Username: {row['username'] or '-'}")
console.print(f" Display Name: {row['display_name'] or '-'}")
# Update if not already cracked
if not row["cracked_email"]:
conn.execute("""
UPDATE hashes SET cracked_email = ?, crack_method = 'manual_verify',
cracked_at = CURRENT_TIMESTAMP
WHERE hash = ?
""", (email, h))
conn.commit()
console.print("[green]Recorded as cracked.[/]")
else:
console.print(f"[dim]No match. Hash {h} not in database.[/]")
conn.close()
@cli.command("hash-email")
@click.argument("email")
def hash_email(email):
"""Show the MD5 hash of an email address."""
h = hashlib.md5(email.lower().strip().encode()).hexdigest()
console.print(f"[cyan]{email}[/] → [green]{h}[/]")
# ─── Interactive Menu ─────────────────────────────────────────────────────────
def interactive_menu(ctx):
conn = init_db(ctx.obj["db"])
custom_wordlist = []
def action_handler(key, tui):
nonlocal custom_wordlist
if key == "1":
def on_source(source):
if not os.path.exists(source):
tui.log(f"[!] File not found: {source}")
return
if source.endswith(".db"):
count = import_from_hash_db(conn, source)
else:
count = import_hashes_from_file(conn, source)
tui.log(f"[+] Imported {count} hashes from {source}")
tui.prompt("Path to .db or .txt file", on_source)
elif key == "2":
def on_path(wl_path):
nonlocal custom_wordlist
if os.path.exists(wl_path):
custom_wordlist = load_wordlist(wl_path)
tui.log(f"[+] Loaded {len(custom_wordlist)} words from {wl_path}")
else:
tui.log(f"[!] File not found: {wl_path}")
tui.prompt("Wordlist file path", on_path)
elif key == "3":
def on_region(region):
if region == "all":
region_val = None
else:
region_val = region
tui.log("[*] Starting crack session...")
cracker = Cracker(conn, custom_wordlist=custom_wordlist)
original_record = cracker._record_crack
def patched_record(hash_val, email, method, hash_lookup):
original_record(hash_val, email, method, hash_lookup)
tui.log(f"[+] CRACKED: {email} ({method})")
cracker._record_crack = patched_record
def do_crack():
cracker.crack(region_focus=region_val)
if cracker.newly_cracked:
tui.log(f"[+] Session complete: {len(cracker.newly_cracked)} new cracks")
else:
tui.log("[-] Session complete: no new cracks")
tui.log(f"[*] Tested {cracker.candidates_tested:,} candidates")
tui.run_threaded(do_crack, status="Cracking...")
tui.prompt("Region focus (us/uk/de/jp/cn/etc, or 'all')", on_region, default="all")
elif key == "4":
rows = conn.execute(
"SELECT hash, domain, username, display_name, cracked_email, crack_method FROM hashes WHERE cracked_email != '' ORDER BY cracked_at DESC LIMIT 50"
).fetchall()
t = Table(box=box.HEAVY, border_style=THEME_COLOR, title=f"[bold {THEME_COLOR}]Cracked Hashes[/]")
t.add_column("Hash", style="dim", max_width=16)
t.add_column("Domain", style="cyan")
t.add_column("User", style="green")
t.add_column("Email", style="bold green")
t.add_column("Method", style="yellow")
for r in rows:
t.add_row(r["hash"][:16], r["domain"], r["username"], r["cracked_email"], r["crack_method"])
tui.show_table(t)
elif key == "5":
rows = conn.execute(
"SELECT hash, domain, username, display_name FROM hashes WHERE cracked_email = '' OR cracked_email IS NULL LIMIT 50"
).fetchall()
t = Table(box=box.HEAVY, border_style=THEME_COLOR, title=f"[bold {THEME_COLOR}]Uncracked Hashes[/]")
t.add_column("Hash", style="dim", max_width=16)
t.add_column("Domain", style="cyan")
t.add_column("User", style="green")
t.add_column("Display Name")
for r in rows:
t.add_row(r["hash"][:16], r["domain"], r["username"], r["display_name"])
tui.show_table(t)
elif key == "6":
def on_file(output_file):
export_csv_fn(conn, output_file)
tui.log(f"[+] Exported to {output_file}")
tui.prompt("Output file", on_file, default="um-crack-export.csv")
elif key == "7":
total = conn.execute("SELECT COUNT(*) FROM hashes").fetchone()[0]
cracked = conn.execute("SELECT COUNT(*) FROM hashes WHERE cracked_email != ''").fetchone()[0]
uncracked = total - cracked
t = Table(box=box.ROUNDED, border_style=THEME_COLOR, title=f"[bold {THEME_COLOR}]Statistics[/]")
t.add_column("Metric", style="bold")
t.add_column("Value", justify="right", style="cyan")
t.add_row("Total Hashes", f"{total:,}")
t.add_row("Cracked", f"[green]{cracked:,}[/]")
t.add_row("Uncracked", f"[yellow]{uncracked:,}[/]")
if total > 0:
t.add_row("Crack Rate", f"{cracked/total*100:.1f}%")
tui.show_table(t)
elif key == "8":
def on_email(email):
h = hashlib.md5(email.lower().strip().encode()).hexdigest()
row = conn.execute("SELECT * FROM hashes WHERE hash = ?", (h,)).fetchone()
if row:
tui.log(f"[+] MATCH: {email} -> {h}")
if not row["cracked_email"]:
conn.execute("UPDATE hashes SET cracked_email=?, crack_method='manual', cracked_at=CURRENT_TIMESTAMP WHERE hash=?",
(email, h))
conn.commit()
tui.log(" Recorded as cracked.")
else:
tui.log(f"[-] No match for {email} (hash: {h})")
tui.prompt("Email to verify", on_email)
elif key == "9":
def on_email(email):
h = hashlib.md5(email.lower().strip().encode()).hexdigest()
tui.log(f"[*] {email} -> {h}")
tui.prompt("Email", on_email)
tui = UmbraTUI(
tool_name=TOOL_NAME,
version=VERSION,
menu_items=MENU_ITEMS,
info_items=CRACK_INFO,
action_handler=action_handler,
stats_fn=lambda: get_db_stats(conn),
theme=THEME_COLOR,
engagement=ctx.obj.get("engagement"),
)
tui.run()
conn.close()
if __name__ == "__main__":
cli()