Hash Length Extension Attack
Exploit MD5 and SHA-1/SHA-256 Merkle-Damgård construction to forge signatures without knowing the secret.
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 hashpumpyimport 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, forgedCLI 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 requestWhich Hashes Are Vulnerable?
| Algorithm | Vulnerable | Notes |
|---|---|---|
| MD5 | ✅ Yes | |
| SHA-1 | ✅ Yes | |
| SHA-256 | ✅ Yes | |
| SHA-512 | ✅ Yes | |
| SHA-3 / Keccak | ❌ No | Sponge construction |
| HMAC | ❌ No | Uses 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