mirror of
https://github.com/nox-project/nox-framework.git
synced 2026-06-09 00:17:30 +00:00
NOX Framework v1.0.0
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user