Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions capabilities/network-ops/tests/test_tool_fixes.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ def _load_module(name: str, filename: str):
nmap_mod = _load_module("nmap_tools", "nmap.py")
certipy_mod = _load_module("certipy_tools", "certipy.py")
impacket_mod = _load_module("impacket_tools", "impacket.py")
cracking_mod = _load_module("cracking_tools", "cracking.py")


# ===================================================================
Expand Down Expand Up @@ -577,3 +578,83 @@ async def mock_wait():
await impacket_mod._kill_relay(proc)
mock_getpgid.assert_called_once_with(99999)
mock_killpg.assert_called()


# ===================================================================
# Cracking: hashcat no-backend detection
# ===================================================================


class TestHashcatNoBackendDetection:
"""hashcat should raise an actionable error when no GPU backend is available."""

@pytest.mark.asyncio
async def test_backend_error_suggests_john_with_known_mode(self, tmp_path):
"""CL_PLATFORM_NOT_FOUND_KHR should suggest john_the_ripper with mapped format."""
hash_file = tmp_path / "hashes.txt"
hash_file.write_text("aad3b435b51404eeaad3b435b51404ee")
wordlist = tmp_path / "wordlist.txt"
wordlist.write_text("password\n")

cracker = cracking_mod.Cracking()

async def fake_execute(cmd, **kwargs):
raise RuntimeError(
"Command failed (255): clGetPlatformIDs(): CL_PLATFORM_NOT_FOUND_KHR"
)

with patch.object(cracking_mod, "execute", side_effect=fake_execute):
with pytest.raises(
RuntimeError, match=r"Use john_the_ripper.*hash_format='nt'"
):
await cracker.hashcat(
hashcat_mode=1000,
hash_file=str(hash_file),
wordlist_path=str(wordlist),
)

@pytest.mark.asyncio
async def test_backend_error_chains_original_exception(self, tmp_path):
"""The original hashcat error should be preserved as __cause__."""
hash_file = tmp_path / "hashes.txt"
hash_file.write_text("aad3b435b51404eeaad3b435b51404ee")
wordlist = tmp_path / "wordlist.txt"
wordlist.write_text("password\n")

cracker = cracking_mod.Cracking()
original = RuntimeError(
"Command failed (255): clGetPlatformIDs(): CL_PLATFORM_NOT_FOUND_KHR"
)

async def fake_execute(cmd, **kwargs):
raise original

with patch.object(cracking_mod, "execute", side_effect=fake_execute):
with pytest.raises(RuntimeError) as exc_info:
await cracker.hashcat(
hashcat_mode=1000,
hash_file=str(hash_file),
wordlist_path=str(wordlist),
)
assert exc_info.value.__cause__ is original

@pytest.mark.asyncio
async def test_non_backend_error_propagates_unchanged(self, tmp_path):
"""Errors unrelated to missing backends should propagate as-is."""
hash_file = tmp_path / "hashes.txt"
hash_file.write_text("badhash")
wordlist = tmp_path / "wordlist.txt"
wordlist.write_text("password\n")

cracker = cracking_mod.Cracking()

async def fake_execute(cmd, **kwargs):
raise RuntimeError("Command failed (1): Hash format mismatch")

with patch.object(cracking_mod, "execute", side_effect=fake_execute):
with pytest.raises(RuntimeError, match=r"Hash format mismatch"):
await cracker.hashcat(
hashcat_mode=1000,
hash_file=str(hash_file),
wordlist_path=str(wordlist),
)
69 changes: 50 additions & 19 deletions capabilities/network-ops/tools/cracking.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@

g_hashcat_lock = asyncio.Lock()

HASHCAT_TO_JOHN: dict[int, str] = {
1000: "nt",
2100: "mscash2",
13100: "krb5tgs",
18200: "krb5asrep",
}

_NO_BACKEND_MARKERS = ("CL_PLATFORM_NOT_FOUND_KHR", "No devices found/left")


class Cracking(Toolset):
"""
Expand Down Expand Up @@ -56,33 +65,53 @@ async def hashcat(
raise FileNotFoundError(f"Hash file {hash_file_path} does not exist.")

if not os.path.exists(wordlist_path):
raise FileNotFoundError(f"Wordlist file {wordlist_path} does not exist.")
raise FileNotFoundError(
f"Wordlist file {wordlist_path} does not exist."
)

logger.info(
f"Cracking {hash_file_path} with mode {hashcat_mode} using wordlist {wordlist_path}"
)

# Execute the cracking command
await execute(
[
"hashcat",
"-m",
str(hashcat_mode),
"-a",
"0",
hash_file_path,
wordlist_path,
"--runtime",
str(max_time_minutes * 60),
"--force",
],
timeout=(max_time_minutes * 60) + 30,
)
try:
await execute(
[
"hashcat",
"-m",
str(hashcat_mode),
"-a",
"0",
hash_file_path,
wordlist_path,
"--runtime",
str(max_time_minutes * 60),
"--force",
],
timeout=(max_time_minutes * 60) + 30,
)
except Exception as e:
msg = str(e)
if any(marker in msg for marker in _NO_BACKEND_MARKERS):
john_fmt = HASHCAT_TO_JOHN.get(hashcat_mode)
if john_fmt:
raise RuntimeError(
f"hashcat failed: no OpenCL/CUDA/HIP backend available. "
f"Use john_the_ripper instead with hash_format='{john_fmt}'."
) from e
raise RuntimeError(
f"hashcat failed: no OpenCL/CUDA/HIP backend available. "
f"Use john_the_ripper instead (hashcat mode {hashcat_mode} "
f"has no automatic john format mapping — consult john documentation)."
) from e
raise
Comment on lines +93 to +107

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 7d206d2 — added three tests: backend marker with known mode, unknown mode, and non-backend error propagation.


# Execute the --show command to get the cracked results
return await execute(["hashcat", "-m", str(hashcat_mode), hash_file_path, "--show"])
return await execute(
["hashcat", "-m", str(hashcat_mode), hash_file_path, "--show"]
)

@tool_method(catch=True, variants=["john", "all"])
@tool_method(catch=True, variants=["hashcat", "john", "all"])
async def john_the_ripper(
self,
hash_format: str,
Expand Down Expand Up @@ -138,4 +167,6 @@ async def john_the_ripper(
)

# Execute the --show command to get the cracked results
return await execute(["john", "--show", f"--format={hash_format}", hash_file_path])
return await execute(
["john", "--show", f"--format={hash_format}", hash_file_path]
)
Loading