Hash Length Extension — forge H(secret‖msg‖extra) from a digest alone
Merkle–Damgård hashes (MD5, SHA-1, most of SHA-2) expose their entire internal state as the digest, so if a MAC is built naively as H(secret ‖ message), an attacker who only knows the digest and len(secret) can compute a valid tag for secret ‖ message ‖ padding ‖ extra — appending privileged content without ever learning the secret.
The signal that gives it away
- The server validates a request by computing hash(secret + data) — a naive keyed MAC, not HMAC — and you can see or infer the exact algorithm (MD5/SHA-1/SHA-256 all qualify; verified against Wikipedia's list of Merkle–Damgård-based functions susceptible to this).
- You know or can brute-force len(secret) — often a comment, a config default, or just small enough to try every length in a loop.
- You have at least one known-valid (message, tag) pair and the goal is to smuggle in extra content the server would otherwise reject — the textbook target is appending something like an extra parameter to flip an authorization check.
- The single fastest disqualifier: if the construction is HMAC, this attack does not apply at all — HMAC's nested H(key ⊕ opad ‖ H(key ⊕ ipad ‖ msg)) structure never exposes a raw post-secret internal state as its output, which is precisely why HMAC exists as the fix (confirmed on Wikipedia's length-extension-attack page: "HMAC also uses a different construction and so is not vulnerable"). Spend five seconds confirming which construction you're looking at before writing any exploit code.
How it's exploited
The core fact: in a Merkle–Damgård hash, the digest *is* the compression function's internal state after the last block — nothing is mixed in or truncated at finalization. That means you can resume hashing exactly where the (unknown) original computation left off.
1. Load the known digest as the initial registers. The digest of secret ‖ message becomes your starting internal state — you never need to see secret itself, only its length and the resulting hash.
2. Reconstruct the original glue padding. Merkle–Damgård padding (0x80 byte, zero bytes, then the 64-bit bit-length field) was appended after secret ‖ message when the original hash was computed, and it depends only on len(secret) + len(message) — both of which you know or can compute. This padding becomes part of the forged message; it doesn't disappear.
3. Continue hashing your chosen `extra` bytes on top of that resumed state. The resulting digest is a valid tag for secret ‖ message ‖ padding ‖ extra, even though you never saw secret.
4. Submit the forged message as `message ‖ padding ‖ extra` (raw bytes or URL-encoded, whatever the transport expects) together with the new digest as the tag. The server, computing H(secret ‖ (message ‖ padding ‖ extra)) on its end, reconstructs the exact same internal-state resumption and the tag matches.
Field-tested tooling — don't hand-roll the padding math. hashpumpy (pip install hashpumpy, Python bindings for the HashPump C tool) does steps 1-3 for you:
``python
import hashpumpy # pip install hashpumpy
new_digest, new_msg = hashpumpy.hashpump(
orig_sig, orig_data, append_data, secret_len)
``
It returns both the new digest (the forged tag) and the new message bytes (including the reconstructed glue padding) — send new_msg as-is, don't try to reassemble it by hand.
Brute-forcing an unknown secret length. If len(secret) isn't given, loop over plausible lengths and try each one against the server's own verification — the padding computation depends on the *combined* length, so a wrong guess produces a tag the server rejects, and a right one succeeds cleanly (no partial credit, no near-misses to interpret):
``python
for L in range(1, 65):
new_digest, new_msg = hashpumpy.hashpump(orig_sig, orig_data, append_data, L)
# try new_msg / new_digest against the oracle; stop at first accept
``
A length ceiling of ~64 covers the overwhelming majority of CTF secrets; widen it only if nothing hits.
Don't forget the padding is visible, not hidden. The forged message you send is original_message ‖ glue_padding ‖ extra — the \x80, the zero-fill, and the 64-bit length field are literal bytes that must reach the server exactly as computed, not something you can omit or the verifier reconstructs a different internal state and rejects the tag. If the transport is a URL parameter or JSON field, URL-encode/escape those bytes rather than assuming they'll survive raw.
Related
Hash Collisions and Weak Preimages — truncated tags and broken functions that fall to brute force or known tooling CRC and Polynomial Recovery — forging or reversing a checksum built from GF(2) division Encoding and Decoding Layers — peel base64/hex/URL/compression first or you attack the wrong plaintext Merkle–Damgård Construction — the digest IS the internal state MAC Forgery — the construction, not the primitive, decides the attack HMAC Fundamentals — the nested MAC that tells you when to STOP trying length-extension CBC-MAC Forgery — splice a new message onto a known tag without the key Sponge Construction — why length-extension does NOT work on SHA-3/Keccak
What does NOT work
- Trying this against HMAC. This is the single most common wasted hour: HMAC's nested double-hash means there is no raw post-secret state to resume from — the outer hash's input already includes a full hash of the inner computation, so "just append bytes and rehash" produces nothing usable. If the MAC is HMAC, pivot immediately to timing/implementation-bug angles instead (HMAC Fundamentals — the nested MAC that tells you when to STOP trying length-extension covers what's actually left to attack there).
- Trying this against SHA-3/Keccak (sponge construction) or the truncated SHA-2 variants. SHA-3 never outputs its full internal state — the sponge's capacity portion stays hidden by design, so there's no state to resume hashing from. Separately, the *truncated* SHA-2 variants — SHA-384, SHA-512/224, and SHA-512/256 — are also not susceptible: each starts from different initial values and simply discards part of the final internal state before outputting the digest, so the attacker never sees the full state either (verified against Wikipedia's length-extension-attack page). This does not extend to full SHA-512 — SHA-512 itself outputs its entire internal state and is just as susceptible as MD5/SHA-1/SHA-256. Don't let the shared "SHA-512" name lead you to rule out the full-width hash by mistake. See Sponge Construction — why length-extension does NOT work on SHA-3/Keccak for why the construction itself closes this off for SHA-3, versus truncation-before-output closing it off for the SHA-2 variants.
- Guessing the padding format instead of computing it exactly. The glue padding is fully determined by the *combined* length of secret+message and the specific hash's byte-order/length-field convention (some use bit-length as big-endian 64-bit, big-endian vs little-endian differs by algorithm family) — an off-by-one on padding length, or using the wrong endianness, produces a digest that looks plausible but never verifies, with no diagnostic beyond "wrong". This is exactly the class of bug hashpumpy/HashPump exists to eliminate — prefer it over reimplementing padding by hand under time pressure.
- Assuming you need to know the secret's contents. You never do, and spending time trying to recover the secret itself (brute force, guessing) when only its *length* matters is wasted effort — the whole point of the attack is that content is irrelevant, only byte-length is.
- Forgetting the original glue padding is part of the forged message, not something you strip. A common early mistake is sending just message ‖ extra without the reconstructed padding in between — the server's own hash computation still inserts padding based on what it thinks is the full input, so omitting it desyncs your forged tag from what the server computes and the check fails silently.
Verified against
18 claims checked against these sources · 1 refuted and removed
What links here
Source: Sinapsi — verified compositional memory, queryable by LLMs. Query this wiki live from your assistant over MCP, or build your own verified wiki (public, or private for your team). CC BY 4.0 — reuse with attribution to Sinapsi.