← Back to Writeups
CUSTOMEasyForensics

Zip, Zip, Hooray!

XESXOR9/10/20263 min read
#forensics#custom#easy#password_pattern_discovery#recursive_archive_extraction#scripted_multi_format_extracti

Zip, Zip, Hooray!

Platform: CUSTOM | Category: Forensics | Type: Challenge | Difficulty: Easy | OS: N/A | **Author: XESXOR Techniques: password_pattern_discovery, recursive_archive_extraction, scripted_multi_format_extraction

Summary

Walkthrough of the CUSTOM Forensics Zip, Zip, Hooray!. Task: a mislabeled challenge.zip is actually a 1250-layer deep chain of recursively nested archives (gzip -> tar -> bzip2 -> password-protected 7z -> zip -> repeat). Solution: script 7z as a universal

Description

"I was trying to compress my files and my script got a little carried away... Can you help me find my original file?"

Hint: the 7z files are password protected, and the password is the name of the first file inside.

Given a single challenge.zip downloaded from the CTFd instance. Goal: recover the original file (which holds the flag) from a runaway recursive compression chain.

Analysis

Despite the .zip extension, file challenge.zip reports gzip-compressed data (was "layer1.tar"), so the extension is misleading — this is a matryoshka-style nested archive.

The chain cycles the same compression pattern once per "round", repeating for hundreds of rounds:

gzip (.tar.gz) -> tar (.tar) -> bzip2 (.bz2) -> 7z (password-protected AES) -> zip -> (back to gzip)

Two gotchas per round:

  • tar @PaxHeader: tar extraction emits an extra @PaxHeader pseudo-file that must be filtered so the "single next file" logic keeps working.
  • 7z password: each 7z layer is AES-encrypted. Per the hint, the password for a 7z archive is the full basename (with extension) of its first contained member. E.g. the 7z named layer290 contains layer292.zip, so its password is layer292.zip.

7z handles gzip, bzip2, tar, zip and 7z transparently, so a single loop using 7z as a universal driver can walk every layer type.

Solution

Automate the traversal:

  1. 7z l -slt <archive> lists contents. The first Path = line is the archive itself; subsequent Path = lines are members. Type = gives the format.
  2. 7z x -y -o<dir> <archive> extracts. If it fails and the type is 7z, retry with -p<candidate> where the candidate is the first member's name (.name, the full filename with extension, is what works; .stem tried as fallback).
  3. Filter out @PaxHeader, then recurse into the single remaining extracted file.

The script traversed 1,250 archive layers. At level 1250, layer1000.zip extracted to flag.txt containing the flag.

python
#!/usr/bin/env python3
from pathlib import Path
import re, shutil, subprocess

root = Path(__file__).resolve().parent
current = root / "challenge.zip"
work = root / "layers"
shutil.rmtree(work, ignore_errors=True)
work.mkdir()

for level in range(1, 10000):
    listing = subprocess.run(["7z", "l", "-slt", str(current)], capture_output=True, text=True)
    archive_type = re.search(r"^Type = (.+)$", listing.stdout, re.M)
    if listing.returncode or not archive_type:
        print(f"Stopped at level {level}: {current} is not an archive")
        print(current.read_bytes()[:1000])
        break
    paths = re.findall(r"^Path = (.+)$", listing.stdout, re.M)[1:]  # skip the archive itself
    out = work / f"{level:04d}"
    out.mkdir()
    command = ["7z", "x", "-y", f"-o{out}", str(current)]
    result = subprocess.run(command, capture_output=True, text=True)
    password = None
    if result.returncode and archive_type.group(1).lower() == "7z" and paths:
        for candidate in (Path(paths[0]).name, Path(paths[0]).stem):
            shutil.rmtree(out); out.mkdir()
            result = subprocess.run(command + [f"-p{candidate}"], capture_output=True, text=True)
            if result.returncode == 0:
                password = candidate
                break
    if result.returncode:
        raise RuntimeError(f"Extraction failed at {current}")
    files = [p for p in out.rglob("*") if p.is_file() and p.name != "@PaxHeader"]
    print(level, archive_type.group(1), current.name, "password=" + repr(password), "->", [p.name for p in files])
    current = files[0]

Key Takeaways / Lessons

  • Mastering password_pattern_discovery, recursive_archive_extraction, scripted_multi_format_extraction was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-07-11 • CUSTOM • Zip, Zip, Hooray! • Difficulty: Easy