feat(hashview): display a customer's hashfiles instead of asking for type

Downloading previously required knowing/typing the hash type because
Hashview only lists hashfiles per type. The download flow now sweeps
common hashcat modes (HashviewAPI.get_all_customer_hashfiles) and shows
all of the customer's uploaded hashfiles to pick from, deduped by id.
A manual hash-type prompt remains as a fallback only when nothing is
found among the common set (uncommon modes). The chosen file's real
hash type still drives the download.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Justin Bollinger
2026-06-18 17:44:48 -04:00
co-authored by Claude Opus 4.8
parent ab4700e636
commit 347e3529d5
4 changed files with 139 additions and 24 deletions
+1 -1
View File
@@ -913,7 +913,7 @@ Interactive menu for downloading and managing wordlists from Weakpass.com via Bi
### Version History
Version 2.10.6
- Fixed the Hashview integration calling API routes that don't exist in Hashview (verified against v0.8.3-dev), which 404'd as soon as a customer ID was entered ("Could not list hashfiles"). The customer→hashfile listing relied on a phantom `GET /v1/hashfiles` list-all route; it now enumerates via the real `GET /v1/hashfiles/hash_type/<type>` endpoint, scoped to the session hash type (prompting for a hash type when none is loaded this session), and filters by customer. Additional client-side route fixes: hashfile hash-type lookup now uses `GET /v1/getHashType/<id>`; "left" (uncracked) hash download uses `GET /v1/hashfiles/<id>`; `delete_job` uses `DELETE /v1/jobs/<id>`; `start_job` uses `POST`. Hashview exposes no stop-job route, so `stop_job` now raises with guidance to use `delete_job`; and no bulk cracked-hash export exists, so the best-effort "found" merge degrades gracefully
- Fixed the Hashview integration calling API routes that don't exist in Hashview (verified against v0.8.3-dev), which 404'd as soon as a customer ID was entered ("Could not list hashfiles"). The customer→hashfile listing relied on a phantom `GET /v1/hashfiles` list-all route; it now enumerates via the real `GET /v1/hashfiles/hash_type/<type>` endpoint. Since that route lists per hash type, the download flow sweeps common hashcat modes to display all of a customer's uploaded hashfiles (falling back to a manual hash-type prompt only if none are found among the common set), then filters by customer. Additional client-side route fixes: hashfile hash-type lookup now uses `GET /v1/getHashType/<id>`; "left" (uncracked) hash download uses `GET /v1/hashfiles/<id>`; `delete_job` uses `DELETE /v1/jobs/<id>`; `start_job` uses `POST`. Hashview exposes no stop-job route, so `stop_job` now raises with guidance to use `delete_job`; and no bulk cracked-hash export exists, so the best-effort "found" merge degrades gracefully
Version 2.10.5
- Pipal analysis no longer corrupts its input when cracked passwords contain `$HEX[...]` rows. `binascii.unhexlify().decode()` returned the bytes without the trailing newline that normal rows inherit from `password[-1]`, so every HEX-encoded password got concatenated with the next one in the `.passwords` file fed to pipal (e.g. three cracks → two lines, one of them a bogus mashup). Pipal then under-counted entries and reported wrong top base words. The HEX branch now re-appends `\n` so each cracked password lands on its own line
+69
View File
@@ -1211,6 +1211,75 @@ class HashviewAPI:
return customer_hfs
# Curated set of hashcat modes commonly seen in engagements. Used to
# enumerate a customer's hashfiles by sweeping the per-type listing
# endpoint, since Hashview exposes no list-all route. Not exhaustive:
# uncommon types can still be queried explicitly via get_customer_hashfiles.
COMMON_HASH_TYPES = (
0, # MD5
100, # SHA1
1000, # NTLM
3000, # LM
1100, # Domain Cached Credentials (DCC), MS Cache
2100, # Domain Cached Credentials 2 (DCC2), MS Cache 2
5500, # NetNTLMv1
5600, # NetNTLMv2
7500, # Kerberos 5 AS-REQ Pre-Auth (etype 23)
13100, # Kerberos 5 TGS-REP (Kerberoasting, etype 23)
18200, # Kerberos 5 AS-REP (AS-REP roasting, etype 23)
19600, # Kerberos 5 TGS-REP (etype 17)
19700, # Kerberos 5 TGS-REP (etype 18)
1800, # sha512crypt $6$ (Linux)
500, # md5crypt $1$ (Linux/Cisco)
7400, # sha256crypt $5$
3200, # bcrypt $2*$
1700, # SHA512
1400, # SHA256
160, # HMAC-SHA1
13400, # KeePass
9600, # MS Office 2013
10500, # PDF 1.4-1.6
11600, # 7-Zip
16500, # JWT
22000, # WPA-PBKDF2-PMKID+EAPOL
)
def get_all_customer_hashfiles(self, customer_id, hash_types=None):
"""Aggregate a customer's hashfiles across hash types (deduped by id).
Hashview has no list-all route, so this sweeps the per-type listing
endpoint (:meth:`get_hashfiles_by_type`) over ``hash_types`` and keeps
the files belonging to ``customer_id``. Defaults to
:attr:`COMMON_HASH_TYPES`; a file of an uncommon type not in the sweep
won't appear. The first hash type a file is seen under is retained as
its ``hash_type`` (mixed-type files are rare).
"""
if hash_types is None:
hash_types = self.COMMON_HASH_TYPES
seen = {}
for ht in hash_types:
try:
files = self.get_hashfiles_by_type(ht)
except Exception as e:
if self.debug:
print(f"[DEBUG] get_all_customer_hashfiles: type {ht} failed: {e}")
continue
for hf in files:
if int(hf.get("customer_id", 0)) != int(customer_id):
continue
hf_id = hf.get("id")
if hf_id is None:
continue
if not (hf.get("hashtype") or hf.get("hash_type")):
hf["hash_type"] = str(ht)
seen.setdefault(int(hf_id), hf)
if self.debug:
print(
f"[DEBUG] get_all_customer_hashfiles({customer_id}): "
f"found {len(seen)} hashfiles across {len(hash_types)} types"
)
return list(seen.values())
def get_customer_hashfiles_with_hashtype(self, customer_id, target_hashtype="1000"):
"""Return hashfiles for a customer that match the requested hashtype."""
customer_hashfiles = self.get_customer_hashfiles(
+34 -23
View File
@@ -3759,34 +3759,45 @@ def hashview_api():
continue
# List hashfiles for the customer. Hashview has no
# list-all route, so we must enumerate by hash type.
# Prefer the session hash type (set when a hashfile is
# loaded); otherwise prompt, since None can't be queried.
download_hash_type = hcatHashType
if not download_hash_type:
ht_input = input(
"\nNo hash type loaded this session. Enter the hashcat "
"hash-type to list (e.g. 1000 for NTLM), or Q to cancel: "
).strip()
if ht_input.lower() == "q":
cancel_download = True
break
if not ht_input.isdigit():
print(
"\n✗ Error: Hash type must be a numeric hashcat mode."
)
continue
download_hash_type = ht_input
# list-all route, so we enumerate by sweeping the
# per-type endpoint across common hashcat modes and
# showing whatever the customer has uploaded.
try:
customer_hashfiles = api_harness.get_customer_hashfiles(
customer_id, hash_type=download_hash_type
print(
"\nScanning customer hashfiles across common hash types..."
)
customer_hashfiles = api_harness.get_all_customer_hashfiles(
customer_id
)
# Fallback: nothing among common types. The file may
# use an uncommon mode, so let the user name it.
if not customer_hashfiles:
print(
f"\nNo hashfiles found for customer ID {customer_id} "
"among common hash types."
)
ht_input = input(
"Enter a specific hashcat hash-type to check "
"(e.g. 1000), or Q to cancel: "
).strip()
if ht_input.lower() == "q":
cancel_download = True
break
if not ht_input.isdigit():
print(
"\n✗ Error: Hash type must be a numeric hashcat mode."
)
continue
customer_hashfiles = (
api_harness.get_customer_hashfiles(
customer_id, hash_type=ht_input
)
)
if not customer_hashfiles:
print(
f"\nNo hashfiles of type {download_hash_type} found "
f"for customer ID {customer_id}"
f"\nNo hashfiles found for customer ID {customer_id}"
)
continue
+35
View File
@@ -95,6 +95,41 @@ class TestHashviewAPI:
result = api.get_customer_hashfiles(1)
assert result == []
def test_get_all_customer_hashfiles_sweeps_and_dedupes(self, api):
"""Aggregate sweeps per-type listings, filters by customer, dedupes by id."""
per_type = {
1000: [
{"id": 1, "customer_id": 1, "name": "ntlm.txt", "hash_type": 1000},
{"id": 2, "customer_id": 2, "name": "other.txt", "hash_type": 1000},
],
5600: [
{"id": 3, "customer_id": 1, "name": "ntlmv2.txt", "hash_type": 5600},
# id 1 appears again under another type; must dedupe (first wins)
{"id": 1, "customer_id": 1, "name": "ntlm.txt", "hash_type": 5600},
],
}
api.get_hashfiles_by_type = Mock(side_effect=lambda ht: per_type.get(int(ht), []))
result = api.get_all_customer_hashfiles(1, hash_types=[1000, 5600])
ids = sorted(hf["id"] for hf in result)
assert ids == [1, 3] # customer 2 excluded, id 1 not duplicated
by_id = {hf["id"]: hf for hf in result}
assert str(by_id[1]["hash_type"]) == "1000" # first type seen wins
assert str(by_id[3]["hash_type"]) == "5600"
def test_get_all_customer_hashfiles_skips_failing_types(self, api):
"""A per-type query that errors is skipped, not fatal."""
def _by_type(ht):
if int(ht) == 1000:
raise RuntimeError("boom")
return [{"id": 9, "customer_id": 1, "name": "x", "hash_type": int(ht)}]
api.get_hashfiles_by_type = Mock(side_effect=_by_type)
result = api.get_all_customer_hashfiles(1, hash_types=[1000, 5600])
assert [hf["id"] for hf in result] == [9]
def test_get_customer_hashfiles(self, api):
"""Filter the type-scoped hashfile list by customer_id (real API if possible)."""
hashview_url, hashview_api_key = self._get_hashview_config()