Hashfile import: hash_type is compared to strings, so the API path (int) skips all normalisation — NTLM hashes stored uppercase are never marked cracked
Nobody has claimed this yet.
- 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 Passwordsnever gains plaintexts from API-imported hashes — the reported symptom.(DYNAMIC) All NTLM Hashesaccumulates case-duplicates of the same hash (update_dynamic_wordlist,utils.py:913).(DYNAMIC) All Usernamesis polluted with full AS-REP ciphertexts imported as usernames (line 743 above).- Instacrack under-reports, so the API response's
instacrackedcount 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
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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