Flag: P2P{INNOVATIONCENTRE1995!}
Recovered password: Innovationcentre1995!
Category: Hash cracking / password recovery
Provided file: 1774262947_autobots.zip (a.k.a. autobots_chall)
Solved: 2026-05-30
Challenge description:
An employee at the Innovation Centre has left on "less than good" terms, we need to get access to his account. Can you recover his password from the provided hash? A wordlist has already been generated, but brute force attempts are failing.
Whilst enumerating the system a password policy was identified that consists of
- 15+ Characters
- Must start with a capital letter
- Must end in four digits, followed by a symbol
Flag Format:
P2P{RECOVEREDPASSWORD}(All Uppercase)
1. Recon
$ unzip -l 1774262947_autobots.zip
autobots/hash.txt (33 bytes)
autobots/wordlist.txt (25968 bytes, 3303 lines)
$ cat autobots/hash.txt
b372f82c401bcd4ac06232f57dfef159
hash.txtis 32 hex characters → a 128-bit digest, almost certainly MD5.wordlist.txtis 3303 words — a CeWL-style scrape of the De Montfort University / Innovation Centre web pages (University,Research,Leicester,Montfort,innovationcentre, …).
2. Reading the challenge correctly
The key sentence is "brute force attempts are failing." The password is 15+ characters, so naive brute force is computationally hopeless — that's the intended trap.
The leaked password policy is actually the attack recipe. It tells us the exact shape of every valid password:
[ Capital-start word ] + [ 4 digits ] + [ 1 symbol ] total length >= 15
This is a classic hybrid attack: take each word from the provided wordlist, capitalize it, and append a 4-digit number plus a symbol.
Search-space reduction: since the total must be ≥ 15 and the suffix is exactly 5 chars
(dddd + symbol), the base word must be ≥ 10 characters. That trims the 3303-word list
down to 425 usable candidate words.
Candidate space:
425 words × 10,000 (0000–9999) × 32 symbols = 136,000,000 candidates
Symbol set = the 32 ASCII specials in Python's string.punctuation:
!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ (here, !).
3. Tooling problem (and the workaround)
The usual tools were unavailable on the box:
- hashcat →
clGetPlatformIDs(): CL_PLATFORM_NOT_FOUND_KHR— no OpenCL/CUDA/HIP backend, so it refuses to run. - john → crashed immediately with SIGILL (exit 132) on this CPU (binary built for an unsupported instruction set), even on a trivial crack.
So I wrote a small multiprocessing hashlib cracker in pure Python instead.
crack.py:
#!/usr/bin/env python3
import sys, hashlib, string, multiprocessing as mp
TARGET_HEX = "b372f82c401bcd4ac06232f57dfef159"
HASHTYPE = sys.argv[1] if len(sys.argv) > 1 else "md5"
MINLEN = int(sys.argv[2]) if len(sys.argv) > 2 else 15
target = bytes.fromhex(TARGET_HEX)
SYMS = string.punctuation # 32 symbols
SUFFIXES = [(("%04d" % d) + s).encode() for d in range(10000) for s in SYMS] # 320,000
def md5_digest(b): return hashlib.md5(b).digest()
def ntlm_digest(b): return hashlib.new('md4', b.decode().encode('utf-16le')).digest()
DIGEST = md5_digest if HASHTYPE == "md5" else ntlm_digest
def load_words():
seen, out = set(), []
for line in open("wordlist.txt", encoding='utf-8', errors='ignore'):
w = line.rstrip('\r\n')
if not w: continue
cap = w[0].upper() + w[1:] # enforce capital start
if not ('A' <= cap[0] <= 'Z'): continue
if len(cap) + 5 < MINLEN: continue # word + dddd + symbol must reach MINLEN
if cap in seen: continue
seen.add(cap); out.append(cap.encode())
return out
def worker(word):
for suf in SUFFIXES:
if DIGEST(word + suf) == target:
return word.decode() + suf.decode()
return None
def main():
words = load_words()
with mp.Pool(mp.cpu_count()) as pool:
for res in pool.imap_unordered(worker, words, chunksize=1):
if res:
print("[+] FOUND:", res)
print("[+] verify:", DIGEST(res.encode()).hex())
pool.terminate(); return
print("[-] not found")
if __name__ == "__main__":
main()
Notes:
- Compares raw
digest()bytes (faster than hex) against the target. - The 320,000 suffixes are precomputed once and shared across workers via
fork. - Includes an
ntlmmode as a fallback in case the 32-hex hash were NTLM rather than MD5.
4. Run
$ python3 crack.py md5 15
[*] type=md5 minlen=15 words=425 suffixes=320000 candidates=136,000,000
...400/425 words done
[+] FOUND: Innovationcentre1995!
[+] md5('Innovationcentre1995!') verify = b372f82c401bcd4ac06232f57dfef159
real 2m59s (4 cores)
Base word innovationcentre is line 2946 of the wordlist; capitalized → Innovationcentre,
then 1995 + !.
5. Verification
| Policy rule | Innovationcentre1995! |
|---|---|
| 15+ characters | ✓ 21 |
| Starts with a capital letter | ✓ I |
| Ends in four digits, then a symbol | ✓ 1995 + ! |
| MD5 == target | ✓ b372f82c401bcd4ac06232f57dfef159 |
$ python3 -c "import hashlib; print(hashlib.md5(b'Innovationcentre1995!').hexdigest())"
b372f82c401bcd4ac06232f57dfef159
Flag format requires the recovered password in all uppercase:
P2P{INNOVATIONCENTRE1995!}
6. Takeaways
- When brute force "fails," a leaked password policy is a gift — it collapses an
intractable keyspace into a precise mask:
word + 4 digits + symbol. - A provided wordlist + policy ⇒ hybrid (wordlist + mask) attack
(
hashcat -a 6 hash wordlist '?d?d?d?d?s'with a capitalize rule, when a GPU/OpenCL backend is available). - The length requirement (15+) lets you pre-filter the wordlist (word ≥ 10 chars), cutting candidates ~8× (3303 → 425 words).
- When
hashcathas no backend andjohnis a broken build, a few lines ofhashlib+multiprocessingis a perfectly good fallback for fast (unsalted MD5) hashes — 136M candidates in ~3 minutes on 4 CPU cores.