hashview / hashview/hashview

Hashfile import: hash_type is compared to strings, so the API path (int) skips all normalisation — NTLM hashes stored uppercase are never marked cracked

Open Beginner friendly
#444 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug Jobs python Wordlists
Dominant language
Python
Stars
399
Forks
52
Avg merge
21h 39m
Merged PRs (30d)
79

Description

Summary

import_hashfilehashes decides how to normalise an incoming hash by comparing hash_type against string literals. The web UI supplies hash_type as a string (form field), so those branches fire. The API route is declared <int:hash_type>, so it supplies an int and every one of those branches is skipped.

The most damaging consequence: NTLM ciphertexts uploaded through POST /v1/hashfiles/upload/... are stored verbatim instead of lowercased. Since hashcat emits hex hashes lowercased, the md5(ciphertext) lookup that ingests crack results never matches those rows — so an API-imported NTLM hash from an uppercase dump can be cracked by an agent over and over and never be marked cracked. Its plaintext therefore never reaches the (DYNAMIC) All Recovered Passwords wordlist, and "instacrack" on import reports 0 against a corpus that already has the answer.

This is the exact failure the code one branch below already warns about (hashview/utils/utils.py:721-723):

hashcat emits hex hashes (e.g. NTLM) lowercased, so store them lowercased too -- otherwise the md5(ciphertext) lookup on crack upload misses

Reproduction

Against the real endpoints (in-memory SQLite, tests/unit fixtures), uploading 8846F7EAEE8FB117AD06BDD830B7586C (NTLM for password) as file_format=5 (hash_only), hash_type=1000:

Step Result
POST /v1/hashfiles/upload/<cust>/5/1000/hf stored as 8846F7EAEE8FB117AD06BDD830B7586C — the same line imported through the UI path (hash_type='1000') is stored 8846f7ea…586c
Agent POST /v1/uploadCrackFile/<jt> with 8846f7ea…586c:<hex plain> cracked=False, plaintext=None — no match
Regenerate (DYNAMIC) All Recovered Passwords empty

With a pre-existing cracked copy of that hash in the DB, the same upload returns "instacracked": 0 and creates a duplicate Hashes row, after which (DYNAMIC) All NTLM Hashes contains both the lowercase and the uppercase form of one hash.

Where

hashview/utils/utils.py, import_hashfilehashes (line 665). Every site below is dead on the API path:

Line Comparison Effect when skipped (API / int)
688 if hash_type in ('300', '1731', '1000') hash_only NTLM/LM/half-NTLM not lowercased
690 elif hash_type == '2100' hash_only DCC2 not lowercased, $dcc2$$DCC2$ normalisation skipped
697 if hash_type == '2100' hash_only DCC2 username never extracted (stays NULL)
711 if hash_type == '300' or hash_type == '1731' user_hash LM/half-NTLM not lowercased
714 elif hash_type == '2100' user_hash DCC2 normalisation skipped
725 if hash_type in ('300', '1731', '1000') user_hash NTLM not lowercased
743 if hash_type == '18200' kerberos AS-REP username is taken as line.split('$')[3] without the : split, so the stored username is the principal plus the whole ciphertext

Two sites in the same function already carry the fix and its rationale — str(hash_type) == '2100' at line 683, and line 708 with the comment "hash_type arrives as an int on the API path (routes.py:takes <int:hash_type>)". The neighbours were missed.

The pwdump and NetNTLM branches are unaffected: pwdump hardcodes hash_type='1000' and lowercases unconditionally, and NetNTLM case-folds by field position rather than by hash type.

Blast radius

  • (DYNAMIC) All Recovered Passwords never gains plaintexts from API-imported hashes — the reported symptom.
  • (DYNAMIC) All NTLM Hashes accumulates case-duplicates of the same hash (update_dynamic_wordlist, utils.py:913).
  • (DYNAMIC) All Usernames is polluted with full AS-REP ciphertexts imported as usernames (line 743 above).
  • Instacrack under-reports, so the API response's instacracked count and the analytics that key off it are wrong for API uploads.
  • Hashes are silently re-cracked on every job because they never flip to cracked=1, and a job's "hashfile fully recovered" stop condition (_hashfile_has_uncracked, api/routes.py:247) can never be satisfied.

Only case-sensitive input is affected: an all-lowercase dump uploaded through the API behaves correctly today, which is why this has gone unnoticed.

Proposal

Normalise once at the top of import_hashfilehashes:

hash_type = str(hash_type)

This matches the existing house pattern in the same module — validate_* and friends already open with hash_type = str(hash_type) (utils.py:1555, :1639, :1788) — and removes the need for the two ad-hoc str(hash_type) calls at lines 683 and 708. Sprinkling more str() calls at each site would leave the same trap for the next branch added.

import_hash_only writes hash_type into an Integer column, so passing a string through is already the status quo on the UI path and needs no change.

Note this only fixes hashes imported from now on. Rows already stored uppercase by the API stay unmatchable; a follow-up data migration lowercasing ciphertext and recomputing sub_ciphertext for the affected hash_types would be needed to recover them, and should be tracked separately.

Test gap

The only existing test that passes an int (tests/unit/test_machine_account_filter.py:133) exercises just the already-fixed machine-account filter at line 708; every other test of import_hashfilehashes passes hash_type as a string, and the API-upload tests in tests/unit/test_api_routes_regression.py only exercise the invalid-file-format branch. Nothing crosses the int/str boundary, which is why the divergence is invisible to the suite.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in hashview/utils/utils.py at import_hashfilehashes around line 665, then review the existing integer-handling tests and the API upload route in api/routes.py. Add coverage for an integer hash_type through the API path and verify it matches the string path for normalization and metadata. Done means the regression passes and affected hashes can be matched and marked cracked.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
api, backend, databases, testing
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
86/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.