Skip to content

How MS Office hashes are extracted

How Hash Extractor reads a password-protected .docx, .xlsx, or .pptx and builds the $office$ hash for hashcat or John the Ripper — entirely in the browser.

Published on 4 min read

Drop a password-protected .docx, .xlsx, or .pptx into Hash Extractor and you get a $office$ hash ready for hashcat or John the Ripper — parsed entirely inside your browser tab, no upload required. This post walks through exactly what the extractor reads to produce that hash and why the file is nothing like a plain ZIP.

The file is not a ZIP — it is an OLE2 container

An unprotected OOXML file really is a ZIP archive containing XML parts. Password protection changes the format entirely. Office wraps the encrypted payload in a Compound File Binary (CFB), also known as OLE2 — the same container format used by legacy .doc and .xls files. The CFB magic bytes are:

D0 CF 11 E0 A1 B1 1A E1

The extractor checks for those eight bytes first. If they match, cfb::CompoundFile::open parses the container and the extractor looks for two named streams inside it:

  • EncryptionInfo — a descriptor that tells Office how the document was encrypted; this is what the extractor reads.
  • EncryptedPackage — the actual OOXML ZIP, AES-encrypted. The extractor ignores this stream.

If no EncryptionInfo stream exists the file is probably a legacy 97-2003 document — OLE2 but with an RC4-based $oldoffice$ scheme that is not handled yet (more on that below).

Version dispatch: standard vs. agile

The first four bytes of EncryptionInfo are a version pair: major (little-endian u16) then minor (little-endian u16). The version determines the encryption scheme:

major.minorSchemeYearhashcat
3.2 or 4.2Standard (binary)2007-m 9400
4.4Agile (XML)2010/2013/2016+-m 9500 or -m 9600

Any other pair is rejected as unsupported.

Standard encryption (2007, -m 9400)

Version 3.2 or 4.2 means Office 2007's binary layout. After the four version bytes comes a flags field (4 bytes), then a header-size field (4 bytes). Using that size the extractor slices out the EncryptionHeader, which holds keySize at offset 16 within the header.

Immediately after the header is the EncryptionVerifier, parsed field by field:

  1. saltSize (4 bytes) — how many salt bytes follow; typically 16.
  2. salt (saltSize bytes) — the password-derivation salt.
  3. encryptedVerifier (16 bytes) — AES-encrypted block used to verify the password.
  4. verifierHashSize (4 bytes) — size of the hash output; 20 for SHA-1.
  5. encryptedVerifierHash (32 bytes) — encrypted hash of the verifier.

The hash string is assembled as:

$office$*2007*<verifierHashSize>*<keySize>*<saltSize>*<salt_hex>*<encVerifier_hex>*<first_64_hex_of_encVerifierHash>

The verifier hash is truncated to 64 hex characters (32 bytes) regardless of the stored size.

Agile encryption (2010+, -m 9500 / -m 9600)

Version 4.4 means the "agile" scheme introduced in Office 2010. The descriptor is not binary — it is an XML document that starts at byte 8 of the stream (bytes 4-7 are a reserved field).

Office can chain multiple encryptors for the same document. The extractor scans the XML for the <p:encryptedKey> element that carries a spinCount attribute — that is the password encryptor, as opposed to key-transport encryptors used for rights management.

From that element's attributes it reads:

  • spinCount — the PBKDF2 iteration count (typically 100 000).
  • keyBits — AES key size in bits (128 or 256).
  • saltSize — salt length in bytes (typically 16).
  • hashAlgorithmSHA1 for 2010, SHA512 for 2013+.
  • saltValue — base-64 encoded salt.
  • encryptedVerifierHashInput — base-64 encoded encrypted block.
  • encryptedVerifierHashValue — base-64 encoded encrypted hash.

The hash algorithm determines which year label and hashcat mode to use:

  • SHA1 / SHA-1 → year 2010, hashcat -m 9500
  • anything else (SHA-512 in practice) → year 2013, hashcat -m 9600

The hash string is:

$office$*<year>*<spinCount>*<keyBits>*<saltSize>*<salt_hex>*<encVerifierInput_hex>*<first_64_hex_of_encVerifierValue>

Worked example

The unit test in office.rs exercises the attribute parser directly with a realistic <p:encryptedKey> tag. The relevant fields resolve to:

spinCount  = 100000
keyBits    = 256
saltSize   = 16
hashAlg    = SHA512  →  year=2013, mode=9600

For a real document the salt and verifier blobs are base-64 decoded and hex-encoded into the final hash string.

A note on experimental status

This format is marked experimental in the tool — the output has not yet been verified against a hash produced by a real Office installation and cracked successfully end-to-end. If you have a known-password .docx and would like to help validate, the feedback is welcome.

Cracking the hash

hashcat (agile 2013+):

echo '$office$*2013*100000*256*16*<salt>*<input>*<value>' > hash.txt
hashcat -m 9600 hash.txt /usr/share/wordlists/rockyou.txt

For Office 2010 swap -m 9600 for -m 9500; for Office 2007 use -m 9400.

John the Ripper auto-detects all three variants:

john --format=office hash.txt

What is not handled: legacy $oldoffice$

Office 97-2003 documents (.doc, .xls, .ppt) are also OLE2 containers, but their encryption is RC4-based — a completely different layout that produces $oldoffice$ hashes. The parser detects the missing EncryptionInfo stream and returns an explicit unsupported error. That scheme is a separate implementation effort.

Why this runs in your browser

The Rust parser compiles to WebAssembly. The CFB is opened, EncryptionInfo is read, and the hash is assembled entirely inside your browser tab. The document bytes never leave your machine. The output mirrors what office2john produces from the command line, so existing cracking pipelines work without modification.

For Office 2013+ files the high iteration count (100 000 by default) is what makes cracking slow even with GPU acceleration — which is exactly the point. A weak password remains the real vulnerability.

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.