mirror of
https://github.com/trustedsec/hate_crack.git
synced 2026-07-28 14:47:22 -07:00
feat: wire non-interactive attack subcommands into the CLI (#17)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
1f0dcb4589
commit
e11a15d4c2
+29
-11
@@ -74,6 +74,7 @@ from hate_crack.cli import ( # noqa: E402
|
|||||||
)
|
)
|
||||||
from hate_crack import attacks as _attacks # noqa: E402
|
from hate_crack import attacks as _attacks # noqa: E402
|
||||||
from hate_crack import llm # noqa: E402
|
from hate_crack import llm # noqa: E402
|
||||||
|
from hate_crack import noninteractive as _noninteractive # noqa: E402
|
||||||
from hate_crack.progress import spinner # noqa: E402
|
from hate_crack.progress import spinner # noqa: E402
|
||||||
from hate_crack.menu import interactive_menu # noqa: E402
|
from hate_crack.menu import interactive_menu # noqa: E402
|
||||||
from hate_crack.username_detect import detect_username_hash_format # noqa: E402
|
from hate_crack.username_detect import detect_username_hash_format # noqa: E402
|
||||||
@@ -760,6 +761,7 @@ hcatGenerateRulesCount = 0
|
|||||||
hcatPermuteCount = 0
|
hcatPermuteCount = 0
|
||||||
hcatProcess: subprocess.Popen[Any] | None = None
|
hcatProcess: subprocess.Popen[Any] | None = None
|
||||||
debug_mode = False
|
debug_mode = False
|
||||||
|
non_interactive = False
|
||||||
hcatUsernamePrefix: bool = False
|
hcatUsernamePrefix: bool = False
|
||||||
|
|
||||||
|
|
||||||
@@ -4058,6 +4060,15 @@ def hashview_api():
|
|||||||
print(f"\nError connecting to Hashview: {str(e)}")
|
print(f"\nError connecting to Hashview: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
def _auto_input(prompt, default=""):
|
||||||
|
"""input() wrapper that returns the default without prompting when running
|
||||||
|
in non-interactive (scripted) mode. In interactive mode this is identical
|
||||||
|
to ``input(prompt) or default``."""
|
||||||
|
if non_interactive:
|
||||||
|
return default
|
||||||
|
return input(prompt) or default
|
||||||
|
|
||||||
|
|
||||||
def _attack_ctx():
|
def _attack_ctx():
|
||||||
ctx = sys.modules.get(__name__)
|
ctx = sys.modules.get(__name__)
|
||||||
if ctx is None:
|
if ctx is None:
|
||||||
@@ -4729,6 +4740,7 @@ def main():
|
|||||||
global hcatHashFileOrig
|
global hcatHashFileOrig
|
||||||
global lmHashesFound
|
global lmHashesFound
|
||||||
global debug_mode
|
global debug_mode
|
||||||
|
global non_interactive
|
||||||
global hashview_url, hashview_api_key
|
global hashview_url, hashview_api_key
|
||||||
global hcatPath, hcatBin, hcatWordlists, hcatOptimizedWordlists, rulesDirectory
|
global hcatPath, hcatBin, hcatWordlists, hcatOptimizedWordlists, rulesDirectory
|
||||||
global pipalPath, maxruntime, bandrelbasewords
|
global pipalPath, maxruntime, bandrelbasewords
|
||||||
@@ -4738,6 +4750,7 @@ def main():
|
|||||||
|
|
||||||
# Initialize global variables
|
# Initialize global variables
|
||||||
hcatHashFile = None
|
hcatHashFile = None
|
||||||
|
non_interactive = False
|
||||||
hcatHashType = None
|
hcatHashType = None
|
||||||
hcatHashFileOrig = None
|
hcatHashFileOrig = None
|
||||||
|
|
||||||
@@ -4824,6 +4837,7 @@ def main():
|
|||||||
return parser, hashview_parser
|
return parser, hashview_parser
|
||||||
|
|
||||||
subparsers = parser.add_subparsers(dest="command")
|
subparsers = parser.add_subparsers(dest="command")
|
||||||
|
_noninteractive.add_attack_subparsers(subparsers)
|
||||||
|
|
||||||
hashview_parser = subparsers.add_parser(
|
hashview_parser = subparsers.add_parser(
|
||||||
"hashview", help="Hashview menu actions"
|
"hashview", help="Hashview menu actions"
|
||||||
@@ -4950,7 +4964,8 @@ def main():
|
|||||||
else:
|
else:
|
||||||
argv = argv_temp # Fallback if subcommand not found
|
argv = argv_temp # Fallback if subcommand not found
|
||||||
|
|
||||||
use_subcommand_parser = "hashview" in argv
|
has_attack_subcommand = bool(argv) and argv[0] in _noninteractive.ATTACK_COMMANDS
|
||||||
|
use_subcommand_parser = "hashview" in argv or has_attack_subcommand
|
||||||
parser, hashview_parser = _build_parser(
|
parser, hashview_parser = _build_parser(
|
||||||
include_positional=not use_subcommand_parser,
|
include_positional=not use_subcommand_parser,
|
||||||
include_subcommands=use_subcommand_parser,
|
include_subcommands=use_subcommand_parser,
|
||||||
@@ -4959,6 +4974,8 @@ def main():
|
|||||||
|
|
||||||
global debug_mode
|
global debug_mode
|
||||||
debug_mode = args.debug
|
debug_mode = args.debug
|
||||||
|
if getattr(args, "command", None) in _noninteractive.ATTACK_COMMANDS:
|
||||||
|
non_interactive = True
|
||||||
|
|
||||||
# CLI flags override config file.
|
# CLI flags override config file.
|
||||||
if getattr(args, "no_potfile_path", False):
|
if getattr(args, "no_potfile_path", False):
|
||||||
@@ -5257,8 +5274,8 @@ def main():
|
|||||||
f"Detected {computer_count} computer account(s)"
|
f"Detected {computer_count} computer account(s)"
|
||||||
" (usernames ending with $)."
|
" (usernames ending with $)."
|
||||||
)
|
)
|
||||||
filter_choice = (
|
filter_choice = _auto_input(
|
||||||
input("Would you like to ignore computer accounts? (Y) ") or "Y"
|
"Would you like to ignore computer accounts? (Y) ", "Y"
|
||||||
)
|
)
|
||||||
if filter_choice.upper() == "Y":
|
if filter_choice.upper() == "Y":
|
||||||
filtered_path = f"{hcatHashFile}.filtered"
|
filtered_path = f"{hcatHashFile}.filtered"
|
||||||
@@ -5280,12 +5297,10 @@ def main():
|
|||||||
)
|
)
|
||||||
) or (lineCount(hcatHashFile + ".lm") > 1):
|
) or (lineCount(hcatHashFile + ".lm") > 1):
|
||||||
lmHashesFound = True
|
lmHashesFound = True
|
||||||
lmChoice = (
|
lmChoice = _auto_input(
|
||||||
input(
|
"LM hashes identified. Would you like to brute force"
|
||||||
"LM hashes identified. Would you like to brute force"
|
" the LM hashes first? (Y) ",
|
||||||
" the LM hashes first? (Y) "
|
"Y",
|
||||||
)
|
|
||||||
or "Y"
|
|
||||||
)
|
)
|
||||||
if lmChoice.upper() == "Y":
|
if lmChoice.upper() == "Y":
|
||||||
hcatLMtoNT()
|
hcatLMtoNT()
|
||||||
@@ -5331,8 +5346,8 @@ def main():
|
|||||||
f"Detected {computer_count} computer account(s)"
|
f"Detected {computer_count} computer account(s)"
|
||||||
" (usernames ending with $)."
|
" (usernames ending with $)."
|
||||||
)
|
)
|
||||||
filter_choice = (
|
filter_choice = _auto_input(
|
||||||
input("Would you like to ignore computer accounts? (Y) ") or "Y"
|
"Would you like to ignore computer accounts? (Y) ", "Y"
|
||||||
)
|
)
|
||||||
if filter_choice.upper() == "Y":
|
if filter_choice.upper() == "Y":
|
||||||
filtered_path = f"{hcatHashFile}.filtered"
|
filtered_path = f"{hcatHashFile}.filtered"
|
||||||
@@ -5411,6 +5426,9 @@ def main():
|
|||||||
else:
|
else:
|
||||||
print("No hashes found in POT file.")
|
print("No hashes found in POT file.")
|
||||||
|
|
||||||
|
if non_interactive:
|
||||||
|
sys.exit(_noninteractive.run_noninteractive(_attack_ctx(), args))
|
||||||
|
|
||||||
# Display Options
|
# Display Options
|
||||||
try:
|
try:
|
||||||
options = get_main_menu_options()
|
options = get_main_menu_options()
|
||||||
|
|||||||
@@ -86,3 +86,60 @@ def run_noninteractive(ctx: Any, args: Any) -> int:
|
|||||||
|
|
||||||
print(f"Error: unknown non-interactive command: {command}")
|
print(f"Error: unknown non-interactive command: {command}")
|
||||||
return 2
|
return 2
|
||||||
|
|
||||||
|
|
||||||
|
ATTACK_COMMANDS = ("quick", "dict", "brute", "topmask")
|
||||||
|
|
||||||
|
|
||||||
|
def add_attack_subparsers(subparsers) -> None:
|
||||||
|
"""Register the non-interactive attack subcommands on an argparse
|
||||||
|
subparsers object (the same one used for ``hashview``).
|
||||||
|
|
||||||
|
Each subcommand carries its own required ``hashfile`` + ``hashtype``
|
||||||
|
positionals plus attack-specific flags.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _add_target(p):
|
||||||
|
p.add_argument("hashfile", help="Path to hash file to crack")
|
||||||
|
p.add_argument("hashtype", help="Hashcat hash type (e.g. 1000 for NTLM)")
|
||||||
|
|
||||||
|
quick = subparsers.add_parser(
|
||||||
|
"quick", help="Non-interactive quick crack (single wordlist + optional rules)"
|
||||||
|
)
|
||||||
|
_add_target(quick)
|
||||||
|
quick.add_argument("--wordlist", required=True, help="Path to wordlist file")
|
||||||
|
quick.add_argument(
|
||||||
|
"--rules",
|
||||||
|
nargs="*",
|
||||||
|
default=[],
|
||||||
|
metavar="RULE",
|
||||||
|
help="Rule filename(s) from the rules directory. Chain with '+' "
|
||||||
|
"(e.g. best64.rule+d3ad0ne.rule). Omit to run without rules.",
|
||||||
|
)
|
||||||
|
|
||||||
|
dictp = subparsers.add_parser(
|
||||||
|
"dict",
|
||||||
|
help="Non-interactive dictionary methodology (uses configured wordlists)",
|
||||||
|
)
|
||||||
|
_add_target(dictp)
|
||||||
|
|
||||||
|
brute = subparsers.add_parser(
|
||||||
|
"brute", help="Non-interactive brute force (mask) attack"
|
||||||
|
)
|
||||||
|
_add_target(brute)
|
||||||
|
brute.add_argument(
|
||||||
|
"--min", type=int, default=1, dest="min_len", help="Minimum length (default 1)"
|
||||||
|
)
|
||||||
|
brute.add_argument(
|
||||||
|
"--max", type=int, default=7, dest="max_len", help="Maximum length (default 7)"
|
||||||
|
)
|
||||||
|
|
||||||
|
topmask = subparsers.add_parser("topmask", help="Non-interactive top-mask attack")
|
||||||
|
_add_target(topmask)
|
||||||
|
topmask.add_argument(
|
||||||
|
"--target-time",
|
||||||
|
type=int,
|
||||||
|
default=4,
|
||||||
|
dest="target_time",
|
||||||
|
help="Target completion time in hours (default 4)",
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
import hate_crack.main as hc_main
|
||||||
from hate_crack import noninteractive as ni
|
from hate_crack import noninteractive as ni
|
||||||
|
|
||||||
|
|
||||||
@@ -161,3 +163,55 @@ def test_dispatch_quick_multiple_rules_runs_each_as_separate_pass(tmp_path):
|
|||||||
f"-r {os.path.join(ctx.rulesDirectory, 'best64.rule')}",
|
f"-r {os.path.join(ctx.rulesDirectory, 'best64.rule')}",
|
||||||
f"-r {os.path.join(ctx.rulesDirectory, 'd3ad0ne.rule')}",
|
f"-r {os.path.join(ctx.rulesDirectory, 'd3ad0ne.rule')}",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _run_main(monkeypatch, argv):
|
||||||
|
monkeypatch.setattr(sys, "argv", ["hate_crack.py"] + argv)
|
||||||
|
with pytest.raises(SystemExit) as excinfo:
|
||||||
|
hc_main.main()
|
||||||
|
return excinfo.value.code
|
||||||
|
|
||||||
|
|
||||||
|
def _make_ntlm_hashfile(tmp_path):
|
||||||
|
# Bare 32-hex NTLM hash: exercises the non-pwdump preprocessing branch.
|
||||||
|
hf = tmp_path / "hashes.txt"
|
||||||
|
hf.write_text("aad3b435b51404eeaad3b435b51404ee\n")
|
||||||
|
return hf
|
||||||
|
|
||||||
|
|
||||||
|
def test_main_quick_dispatches_without_prompting(monkeypatch, tmp_path):
|
||||||
|
hf = _make_ntlm_hashfile(tmp_path)
|
||||||
|
wl = tmp_path / "rockyou.txt"
|
||||||
|
wl.write_text("password\n")
|
||||||
|
calls = []
|
||||||
|
monkeypatch.setattr(
|
||||||
|
hc_main, "hcatQuickDictionary", lambda *a, **k: calls.append((a, k))
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"builtins.input",
|
||||||
|
lambda *a, **k: (_ for _ in ()).throw(AssertionError("prompted")),
|
||||||
|
)
|
||||||
|
code = _run_main(monkeypatch, ["quick", str(hf), "1000", "--wordlist", str(wl)])
|
||||||
|
assert code == 0
|
||||||
|
assert len(calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_main_brute_dispatches(monkeypatch, tmp_path):
|
||||||
|
hf = _make_ntlm_hashfile(tmp_path)
|
||||||
|
calls = []
|
||||||
|
monkeypatch.setattr(hc_main, "hcatBruteForce", lambda *a, **k: calls.append(a))
|
||||||
|
code = _run_main(
|
||||||
|
monkeypatch, ["brute", str(hf), "1000", "--min", "1", "--max", "8"]
|
||||||
|
)
|
||||||
|
assert code == 0
|
||||||
|
assert calls[0][2] == 1 and calls[0][3] == 8
|
||||||
|
|
||||||
|
|
||||||
|
def test_main_quick_bad_hashtype_errors(monkeypatch, tmp_path):
|
||||||
|
hf = _make_ntlm_hashfile(tmp_path)
|
||||||
|
wl = tmp_path / "w.txt"
|
||||||
|
wl.write_text("x\n")
|
||||||
|
code = _run_main(
|
||||||
|
monkeypatch, ["quick", str(hf), "notanumber", "--wordlist", str(wl)]
|
||||||
|
)
|
||||||
|
assert code != 0
|
||||||
|
|||||||
Reference in New Issue
Block a user