mirror of
https://github.com/nox-project/nox-framework.git
synced 2026-06-08 16:07:17 +00:00
120 lines
4.8 KiB
Python
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
|