Here’s our solution script.
from pwn import remote
def split_into_blocks(data: bytes, block_size: int = 16) -> list[bytes]:
"""
Split data into blocks of block_size bytes.
"""
return [data[i : i + block_size] for i in range(0, len(data), block_size)]
def crack_block(conn, ciphertext_block: bytes, block_size: int, iteration: int) -> bytes:
"""
Perform a byte-by-byte padding oracle attack on a single block.
Returns the recovered intermediate (zero IV) bytes.
"""
recovered = bytearray()
# Work from last byte to first
for pad_len in range(1, len(ciphertext_block) + 1):
for guess in range(256):
# Build forged IV:
# - Leading zeros for previous blocks
# - Zeros to align current pad
# - Guess byte
# - Known intermediate bytes XORed with pad length
prefix = b"\x00" * (block_size * iteration)
padding = b"\x00" * (block_size - pad_len)
guess_byte = bytes([guess])
suffix = bytes((b ^ pad_len) for b in recovered)
forged_iv = prefix + padding + guess_byte + suffix
# Send payload as hex string
conn.recvuntil(b"> ")
conn.sendline(forged_iv.hex().encode())
response = conn.recvline()
# Check for successful padding
if b"Padding error" not in response:
# Recover this intermediate byte
recovered.insert(0, guess ^ pad_len)
break
return bytes(recovered)
def break_repeating(conn, ciphertext_blocks: list[bytes], block_size: int) -> bytes:
"""
Orchestrate the attack over multiple ciphertext blocks (here simplified to first block).
"""
# Wait for prompt and send a dummy to sync
conn.recvuntil(b" > ")
conn.sendline(b"00" * block_size)
conn.recvline()
# Attack only the first block in this challenge
return crack_block(conn, ciphertext_blocks[0], block_size, iteration=1)
def main():
host = 'hash.chal.cyberjousting.com'
port = 1351
conn = remote(host, port)
# Read initial banner and ciphertext
conn.recvlines(2)
hex_ct = conn.recvline().strip().decode()
ciphertext = bytes.fromhex(hex_ct)
# Split into blocks if needed; here block_size is 20
block_size = 20
ct_blocks = split_into_blocks(ciphertext, block_size)
# Recover zero IV for the first block
zero_iv = break_repeating(conn, ct_blocks, block_size)
# Append known prefix to form the full zero IV
known_prefix = bytes.fromhex('541b5e3a73645cd0f64f07a5c96c0c7a1887fdc')
full_zero_iv = known_prefix + zero_iv
# Compute plaintext by XORing zero IV with ciphertext prefix
plaintext = bytes(z ^ c for z, c in zip(full_zero_iv, ciphertext[: len(full_zero_iv)]))
print(f"Zero IV: {full_zero_iv!r}")
print(plaintext)
conn.close()
if __name__ == "__main__":
main()