Files
nox-project-nox-framework/sources/helpers/cracker.py
T
2026-04-07 10:17:43 +02:00

120 lines
4.8 KiB
Python

"""
sources/helpers/cracker.py
Resilient async hash cracker for NOX autoscan.
Detects MD5 / SHA1 / SHA256 / bcrypt hashes inside breach records,
fires background crack attempts against available APIs, and returns
results without ever blocking the main pivot pipeline.
"""
import asyncio
import logging
import re
from typing import List, Optional, Tuple
# C2: MD5 and NTLM share the same 32-char hex pattern.
# We list md5 first (most common in breach data) but also accept ntlm
# so callers can query NTLM-specific APIs when needed.
_PATTERNS: List[Tuple[str, re.Pattern]] = [
("bcrypt", re.compile(r"^\$2[aby]?\$\d{2}\$.{53}$")),
("sha256", re.compile(r"^[a-f0-9]{64}$", re.I)),
("sha1", re.compile(r"^[a-f0-9]{40}$", re.I)),
("md5", re.compile(r"^[a-f0-9]{32}$", re.I)),
# ntlm shares the 32-char hex pattern — detected as md5 first,
# but async_crack queries both md5 and ntlm APIs for 32-char hashes.
]
# Writes to ~/.config/nox-cli/logs/nox_system.log — never to terminal
_syslog = logging.getLogger("nox.system")
# Per-API timeout — each individual rainbow-table query budget
_API_TIMEOUT = 8
# Global crack budget — hard cap regardless of API count or response order
CRACK_TIMEOUT = 20
def detect_hash(value: str) -> Optional[str]:
"""Return hash type string if value matches a known hash pattern, else None."""
v = value.strip()
for htype, pat in _PATTERNS:
if pat.match(v):
return htype
return None
async def _query_api(session, url: str, fmt: str) -> Optional[str]:
"""Single API query — returns plaintext or None. Never raises."""
try:
import aiohttp
to = aiohttp.ClientTimeout(total=_API_TIMEOUT)
async with session.get(url, timeout=to) as resp:
if resp.status != 200:
return None
if fmt == "text":
text = (await resp.text()).strip()
# Reject empty, too-long, or obvious error responses
if not text or len(text) > 128:
return None
tl = text.lower()
if any(tl.startswith(p) for p in ("not found", "error", "invalid", "no result", "not in", "cmd5-error", "not exist", "code erreur", "erreur", "unknown")):
return None
return text
data = await resp.json(content_type=None)
return data.get("result") or data.get("plaintext") or data.get("plain") or None
except Exception:
return None
async def async_crack(session, hash_value: str, hash_type: str) -> Optional[str]:
"""
Query multiple rainbow-table APIs concurrently.
Returns first plaintext found, or None. bcrypt is skipped.
C1: create tasks upfront for cancellation, but await each via asyncio.shield
inside as_completed — no double wait_for wrapping.
C2: for 32-char hex (md5/ntlm ambiguity), also query NTLM-specific APIs.
Per-API timeout: 8s. Global budget: 20s (CRACK_TIMEOUT).
All tasks are cancelled as soon as the first result is found.
"""
if hash_type == "bcrypt":
return None
h = hash_value.strip().lower()
apis = [
(f"https://www.nitrxgen.net/md5db/{h}", "text"),
(f"https://hashes.com/en/api/hash?hash={h}", "json"),
(f"https://hash.help/api/lookup/{h}", "json"),
(f"https://hashkiller.io/api/search.php?hash={h}", "json"),
(f"https://md5decrypt.net/Api/api.php?hash={h}&hash_type={hash_type}&email=&code=", "text"),
(f"https://www.cmd5.org/api.ashx?hash={h}", "text"),
]
# C2: for 32-char hashes (md5/ntlm ambiguous), add NTLM-specific endpoint
if hash_type == "md5" and len(h) == 32:
apis.append((f"https://hashes.com/en/api/hash?hash={h}&type=ntlm", "json"))
# C1: create tasks so we can cancel them; shield each before passing to wait_for
# so cancellation of the shield future does not cancel the underlying task prematurely.
tasks = [asyncio.create_task(_query_api(session, url, fmt)) for url, fmt in apis]
result: Optional[str] = None
try:
for fut in asyncio.as_completed(tasks):
try:
res = await asyncio.wait_for(asyncio.shield(fut), timeout=_API_TIMEOUT)
except (asyncio.TimeoutError, asyncio.CancelledError):
continue
except Exception:
continue
if res:
result = res
break
except Exception:
pass
finally:
# Cancel all remaining tasks and await to suppress pending-task warnings
for t in tasks:
if not t.done():
t.cancel()
await asyncio.gather(*[t for t in tasks if not t.done()], return_exceptions=True)
return result