Athena Wiki

Hash Length Extension Attack

cryptointermediate

Exploit MD5 and SHA-1/SHA-256 Merkle-Damgård construction to forge signatures without knowing the secret.

cryptohashlength-extensionmd5sha1sha256hmac-forgery

Hash Length Extension Attack

A hash length extension attack exploits the Merkle-Damgård construction used by MD5, SHA-1, SHA-256 (not SHA-3 or HMAC). If you know H(secret || message) and the length of secret, you can compute H(secret || message || padding || extension) without knowing secret.

Why It Works

MD5/SHA-1/SHA-256 process data in fixed blocks (512 bits). The internal state at the end of one block becomes the initial state for the next. If you can initialize a hash with a known intermediate state, you can continue hashing from there.

Given:

  • H(secret || message) (the known hash)
  • len(secret) (often guessable or given)

You can compute:

  • H(secret || message || padding || evil_extension)

And the valid MAC for this extended message without knowing secret.

What "Padding" Looks Like

MD5/SHA padding appended to a message before hashing:

0x80                    ← padding start byte
0x00 * (padding bytes)  ← zero padding to fill block
message_length_in_bits  ← 8-byte little-endian length (MD5) / big-endian (SHA)

For a secret + message of 23 bytes, padding to 64 bytes = 41 bytes of padding.

Automated Tool: hashpumpy

pip install hashpumpy
import hashpumpy

# Inputs
known_hash   = "aabbccddeeff..."   # H(secret || original_message) in hex
original_msg = b"data=hello"
extension    = b"&admin=true"
secret_len   = 8                    # length of the secret key

# Output: (new_hash, forged_message)
new_hash, forged_message = hashpumpy.hashpump(
    known_hash,
    original_msg,
    extension,
    secret_len
)

print(f"New hash:    {new_hash}")
print(f"Forged msg:  {forged_message.hex()}")

The forged_message includes the padding bytes - send this as the data parameter and new_hash as the signature.

Manual Attack (Python)

import struct, hashlib

def md5_padding(msg_len):
    """Compute the padding appended to a message of msg_len bytes."""
    pad = b'\x80'
    pad += b'\x00' * ((55 - msg_len) % 64)
    pad += struct.pack('<Q', msg_len * 8)  # little-endian for MD5
    return pad

def md5_extend(known_hash, secret_len, original_msg, extension):
    # Extract the intermediate MD5 state from the known hash
    state = struct.unpack('<IIII', bytes.fromhex(known_hash))

    # Total length hashed so far (secret + message + its padding)
    total_len = secret_len + len(original_msg)
    padded_len = total_len + len(md5_padding(total_len))

    # Continue MD5 from the known state, hashing the extension
    # (Use a custom MD5 that accepts a starting state - see hashpumpy source)
    import hashpumpy
    new_hash, forged = hashpumpy.hashpump(known_hash, original_msg, extension, secret_len)
    return new_hash, forged

CLI Tool: hash_extender

git clone https://github.com/iagox86/hash_extender
cd hash_extender && make

./hash_extender \
  --data "original_message" \
  --secret 8 \
  --append "&admin=true" \
  --signature aabbccddeeff... \
  --format md5

# Output:
# New signature: <new_hash>
# New string (hex): <forged_message_hex>

Typical CTF Scenario

1. You log in and get: token = MD5(secret + username)
2. You send token + username to the server; it verifies with the same formula
3. You want to log in as admin but don't know the secret

→ Use length extension:
   Forge token for (username || padding || "admin")
   Send the forged token + forged data as your request

Which Hashes Are Vulnerable?

AlgorithmVulnerableNotes
MD5✅ Yes
SHA-1✅ Yes
SHA-256✅ Yes
SHA-512✅ Yes
SHA-3 / Keccak❌ NoSponge construction
HMAC❌ NoUses two hash passes with key
BLAKE2❌ No

Checklist

  • Is the MAC computed as Hash(secret || message)? → Vulnerable
  • Is HMAC used instead? → Not vulnerable
  • Identify the hash algorithm (md5/sha1/sha256)
  • Figure out or guess the secret length
  • Use hashpumpy or hash_extender to forge
  • Encode the forged message correctly (URL-encode padding bytes!)
  • Note: forged_message contains raw bytes 0x80, 0x00 - encode them!

Last updated on

On this page