$cat secret-service.mdCryptoHackultet 2026
Secret Service
A decryption service that only tells you whether padding looked valid is enough, one byte at a time, to recover an entire AES-CBC encrypted message without ever knowing the key.
$man secret-service
What this one teaches:
- What PKCS#7 padding is, and how CBC decryption depends on the previous ciphertext block.
- The padding oracle attack, recovering plaintext one byte at a time using only a valid-or-invalid padding signal.
Task description
An encrypted message and a service that accepts encrypted data in return.
What’s provided
A single base64 string decodes to exactly 32 bytes: a 16-byte IV followed by one 16-byte AES block of ciphertext. Connecting to the service and sending the original data back unmodified gets Data recovered; sending it with the last byte of the IV flipped gets Incorrect padding instead.
That difference in wording is the entire vulnerability. AES-CBC decrypts a block, then XORs the result with the previous block (here, the IV) to get the real plaintext, and PKCS#7 padding means the decrypted plaintext’s last byte tells you how many padding bytes to expect and strip. Flipping a byte of the IV changes the corresponding byte of the recovered plaintext directly; flip a byte that isn’t part of the padding (byte 0, say) and the message changes but the padding at the end stays valid, so the oracle still reports success. Flip the very last byte, though, and the padding itself breaks, which is exactly what produced Incorrect padding here. A service that distinguishes these two failure modes is a textbook padding oracle (Vaudenay, 2002).
Recovering a byte at a time
The attack works from the last plaintext byte backward. For each position, working right to left:
- The already-recovered suffix of the plaintext is used to compute what a valid padding value (one more than the current position, counting from the end) would require of the intermediate cipher output at each already-known position, and the test IV’s corresponding bytes are set to force exactly that.
- All 256 possible values are tried at the current unknown byte position.
- Whichever value makes the oracle respond
Data recoveredreveals the intermediate state (the AES block’s raw decrypted output, before CBC’s XOR with the IV) at that position, since it’s the one value that happens to produce valid padding. - XORing that intermediate value with the real IV’s byte at that position recovers the actual plaintext byte.
for byte_pos in range(15, -1, -1):
pad_val = 16 - byte_pos
test_iv = bytearray(16)
for k in range(byte_pos + 1, 16):
test_iv[k] = intermediate[k] ^ pad_val
for guess in range(256):
test_iv[byte_pos] = guess
if oracle(test_iv + cipher_block):
intermediate[byte_pos] = guess ^ pad_val
plaintext[byte_pos] = intermediate[byte_pos] ^ iv[byte_pos]
break
Sixteen rounds of at most 256 oracle queries each recover the full 16-byte plaintext block.
Reading the result
b'124867614619\x04\x04\x04\x04'
The trailing four \x04 bytes are exactly what valid PKCS#7 padding for a 12-byte message in a 16-byte block looks like, confirming the recovery is correct. The message itself, 124867614619, is the flag’s content:
CTF2026[124867614619]
Why this works without ever touching the key
Nothing in this attack recovers, guesses, or needs the AES key at any point. The padding oracle leaks information about the relationship between the ciphertext and the plaintext (through CBC’s XOR step) directly, entirely independent of what the encryption key actually was, which is exactly why a correct, well-chosen encryption algorithm isn’t enough by itself; the service around it has to avoid leaking a distinguishable “padding was valid” signal in the first place.
$cat flag.txt reveal
CTF2026[124867614619]