Skeleton
Skeleton
Platform: tjctf | Category: Forensics | Type: Challenge | Difficulty: Easy | OS: Linux | Author: XESXOR | Date: 2026-05-16 | Status: Solved Techniques: bkcrack_key_recovery, png_header_known_plaintext, zip2john_hash_parsing, zipcrypto_known_plaintext_attack
Summary
Task: zip2john hash containing a ZipCrypto-encrypted stored PNG, no original ZIP or password. Solution: extract the encrypted blob from the hash, use bkcrack known-plaintext attack with PNG header/footer bytes to recover internal ZipCrypto keys, then decrypt the image.
Skeleton — TJCTF 2026
Description
I zipped up a picture of the flag, but I forgot the password. Luckily, I saved the zip2john hash. Can you recover the image?
We are given hash.txt containing a zip2john hash — not the original ZIP file. The hash encodes the full ZipCrypto-encrypted blob of a stored (uncompressed) PNG file named flag.png. The goal is to recover the image and read the flag.
Analysis
The hash line follows the $pkzip2$ format:
flag.zip/flag.png:$pkzip2$1*1*2*0*12c*120*c8a6617a*0*26*0*12c*c8a6*81bd*36bee6...221c9*$/pkzip2$:flag.png:flag.zip::flag.zip
Key fields parsed from the *-separated block:
| Field | Value | Meaning |
|---|---|---|
| Compression | 0 | Stored (no compression) |
| Encrypted length | 0x12c = 300 | Total encrypted bytes |
| Uncompressed length | 0x120 = 288 | Original file size |
| Difference | 300 − 288 = 12 | ZipCrypto encryption header |
Since the file is stored (not deflated), the plaintext inside the encrypted blob is the raw PNG bytes. PNG files have a highly predictable structure:
- First 16 bytes (PNG signature + IHDR chunk header):
89 50 4e 47 0d 0a 1a 0a 00 00 00 0d 49 48 44 52 - Last 12 bytes (IEND chunk):
00 00 00 00 49 45 4e 44 ae 42 60 82at offset 276 (= 288 − 12)
This gives 28 bytes of known plaintext — far more than the ~12 bytes bkcrack needs to recover ZipCrypto internal keys.
Solution
Step 1: Extract the encrypted blob from the hash
The 300-byte encrypted ciphertext is embedded in the hash as a hex string. Parse it out:
python read-only • copy allowed#!/usr/bin/env python3 from pathlib import Path line = Path("hash.txt").read_text().strip() # Extract the $pkzip2$...*$/pkzip2$ core core = line.split(':')[1] core = core[len('$pkzip2$'):-len('$/pkzip2$')] # Split on '*' — field 13 (0-indexed) is the encrypted hex blob fields = core.split('*') cipher_hex = fields[13] Path("cipher.hex").write_text(cipher_hex + "\n")
Convert hex to binary:
bash read-only • copy allowedxxd -r -p cipher.hex cipher.bin wc -c cipher.bin # 300 bytes
Step 2: Prepare known plaintext
Create the 16-byte PNG header as a binary file:
bash read-only • copy allowedprintf '89504e470d0a1a0a0000000d49484452' | xxd -r -p > png_header.bin
The IEND chunk bytes will be passed via bkcrack's -x (extra plaintext at offset) option.
Step 3: Recover ZipCrypto internal keys with bkcrack
Run bkcrack with the known plaintext at the start and the IEND chunk at offset 276:
bash read-only • copy allowedbkcrack -c cipher.bin -p png_header.bin -x 276 0000000049454e44ae426082
bkcrack recovers the three 32-bit internal keys:
Keys: c639d1ca b1fd3d6c 25bb9b08
Step 4: Decrypt the image
Use the recovered keys to decrypt:
bash read-only • copy allowedbkcrack -c cipher.bin -k c639d1ca b1fd3d6c 25bb9b08 -d flag.png
Result: a valid 220×20 1-bit grayscale PNG containing the flag text.
bash read-only • copy allowed$ file flag.png flag.png: PNG image data, 220 x 20, 1-bit grayscale, non-interlaced
The image displays: tjctf{REDACTED}
What Didn't Work
- Password cracking with hashcat/john — standard dictionary attacks (rockyou, masks, rules) found CRC-32 collision candidates but not the real password. The challenge was designed to be unsolvable via password cracking.
- Reconstructing a ZIP file — attempting to wrap the ciphertext back into a ZIP container and running bkcrack against it added unnecessary complexity. Working directly with the raw cipher blob is simpler.