Skip to content

How ZIP password hashes are extracted

A deep look at how Hash Extractor pulls crackable hashes from encrypted ZIPs — WinZip AES and legacy ZipCrypto — entirely in the browser.

Published on 6 min read

A ZIP file can carry two fundamentally different encryption schemes. Drop one into Hash Extractor and it will hand you a hash ready for hashcat or John the Ripper — no upload, no server, pure WebAssembly in your browser. This post walks through exactly what the extractor reads off disk to build that hash, and why the two schemes demand very different treatment.

The central directory: the reliable index

The ZIP format has two places where metadata lives: local file headers (written just before each file's compressed data) and the central directory (written after all file data, near the end of the archive). When a ZIP is created in a streaming context — a pipe, a network connection — the compressor does not know the CRC or compressed size until after it finishes writing the data. It sets bit 3 (FLAG_SIZES_IN_DD) in the local header's general-purpose bit flag and writes those values later in a trailing data descriptor instead. This means reading local headers alone can yield zeroes for the fields you care about most.

The extractor therefore starts from the end-of-central-directory (EOCD) record, walks back to the central directory, and enumerates entries from there. The central directory always has correct sizes and CRC values regardless of how the archive was produced. Each central-directory record also stores the byte offset of its corresponding local header, so jumping to the file data is a single seek.

WinZip AES ($zip2$, hashcat -m 13600)

When an entry's compression method is 99 (METHOD_AES), the file was encrypted with WinZip AES. The AES key strength is not in the method field itself — it is buried inside a vendor extra field with tag 0x9901. The extractor scans the extra-field block of the central-directory entry for that tag and reads byte 4 of the field payload: 1 = AES-128, 2 = AES-192, 3 = AES-256.

The strength value directly controls the salt length:

StrengthKey sizeSalt bytes
1128-bit8
2192-bit12
3256-bit16

Once the strength is known, the extractor jumps to the file data (past the local header, using the stored offset plus the filename and extra-field lengths). The compressed payload is laid out as:

[ salt (8/12/16 B) ][ pwd_verify (2 B) ][ ciphertext ][ auth_code (10 B) ]

The two-byte password-verification value lets a cracker quickly reject wrong candidates without running the full AES decryption. The ten-byte authentication code (HMAC-SHA1 truncated) is the integrity check a successful crack must reproduce.

From those four slices the hash is assembled:

$zip2$*0*<strength_hex>*0*<salt_hex>*<verify_hex>*<ct_len_hex>*<ct_hex>*<auth_hex>*$/zip2$

Legacy ZipCrypto ($pkzip$, hashcat -m 17200/17210)

ZipCrypto predates AES by decades. It uses a stream cipher seeded from three 32-bit keys that are updated as the password bytes are mixed in. The cipher is weak because it leaks keystream bytes that can be matched against known plaintext — the compression header at the start of a deflated file, or any repeated byte in a stored file. The zip -e flag on most Unix systems still produces ZipCrypto archives.

The 2-byte checksum

Each encrypted entry begins with a 12-byte header. The last byte of that header is a checksum used to confirm the password guess before the cipher is fully evaluated. Where that checksum byte comes from depends on how the archive was written:

  • If bit 3 is set (FLAG_SIZES_IN_DD): sizes streamed, so the CRC was not known at write time. The checksum is taken from the modification timestamp — the high byte of the 16-bit MS-DOS time field.
  • If bit 3 is clear: the CRC was written into the local header. The checksum comes from the high two bytes of the CRC32 (crc >> 24 and (crc >> 16) & 0xff).

The extractor checks the flag and picks the right source, then hex-encodes the two bytes as the cs value in the hash.

Multi-file strategy: one inline, the rest partial

$pkzip$ supports multiple encrypted files in the same hash to let hashcat/john test a password across several entries simultaneously — this closes the false-positive window. The extractor collects all eligible encrypted entries (method 0 or 8, decompressed size ≥ 4 bytes), sorts them by compressed size ascending, and caps the list at eight.

  • Smallest file — data type 2 (full inline): The entire ciphertext is embedded in the hash. The hash fields include the compressed size, decompressed size, CRC, absolute offset of the local header, the local-header extra overhead (offex), compression method, and the full ciphertext blob.
  • All other files — data type 1 (partial): Only the first 36 bytes of ciphertext are included. That is enough for the cracker to verify the password stream without bloating the hash string.

The hash also carries a check_bytes flag (field B): 1 if any entry requires ZIP version ≥ 2.0, otherwise 2.

Real example

The test suite bundles a fixed archive created with zip -X -P pw. The extractor produces:

$pkzip$1*2*2*0*24*18*711b0d3c*0*25*0*24*10b0*849d49b8e158ec5aa5814d1757060014bd79ab2e00225a24f28c77bcf59151793faec6a2*$/pkzip$

Breaking the full inline entry (DT=2) down:

FieldValueMeaning
DT2full inline
00compression info (reserved)
240x24 = 36compressed size
180x18 = 24decompressed size
711b0d3cCRC32
0local-header offset
25offex — local header overhead bytes
00method (stored)
24compressed size again
10b0cs — checksum bytes from CRC high bytes
849d49b8…full ciphertext blob

The hashcat mode resolves to -m 17210 (PKZIP uncompressed) because the file was stored without deflate.

Crack it

WinZip AES (hashcat):

echo '$zip2$*0*3*0*<salt>*<verify>*<len>*<ct>*<auth>*$/zip2$' > hash.txt
hashcat -m 13600 hash.txt /usr/share/wordlists/rockyou.txt

ZipCrypto compressed (hashcat):

echo '$pkzip$...' > hash.txt
hashcat -m 17200 hash.txt /usr/share/wordlists/rockyou.txt

ZipCrypto uncompressed (hashcat):

hashcat -m 17210 hash.txt /usr/share/wordlists/rockyou.txt

John the Ripper handles both automatically:

john --format=ZIP hash.txt       # WinZip AES
john --format=PKZIP hash.txt     # ZipCrypto

Why this runs in your browser

Hash Extractor compiles the Rust extractor to WebAssembly. The file is parsed entirely inside your browser tab — the archive bytes never leave your machine. The output mirrors what zip2john produces from the same file, so existing cracking workflows work unchanged.

ZipCrypto remains common despite its age because zip -e defaults to it on Linux and macOS. WinZip AES is the modern answer: AES-256 with a proper PBKDF2-derived key and HMAC authentication. Both are crackable given a weak password, and now you know exactly which bytes make that possible.

Related articles

Why 7z is the trickiest archive to crack: LZMA-compressed headers, AES coder property encoding, and the full $7z$ hash format explained.
Inside the adb backup text header: how PBKDF2 salts, round counts, and a master-key blob become a crackable $ab$ hash.
A technical walkthrough of how Hash Extractor parses Ansible Vault files to produce a crackable $ansible$ hash for hashcat and John the Ripper.