← Back to Writeups
CUSTOMEasyForensics

Skeleton

XESXOR9/2/20263 min read
#forensics#custom#easy#bkcrack_key_recovery#png_header_known_plaintext#zip2john_hash_parsing#zipcrypto_known_plaintext_atta#linux

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:

FieldValueMeaning
Compression0Stored (no compression)
Encrypted length0x12c = 300Total encrypted bytes
Uncompressed length0x120 = 288Original file size
Difference300 − 288 = 12ZipCrypto 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 82 at 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
#!/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
xxd -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
printf '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
bkcrack -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
bkcrack -c cipher.bin -k c639d1ca b1fd3d6c 25bb9b08 -d flag.png

Result: a valid 220×20 1-bit grayscale PNG containing the flag text.

bash
$ 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. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍