← back to writeups

Padding oracle: decrypting without ever knowing the key

When the server distinguishes "invalid padding" from "valid padding", it leaks enough information to reconstruct the plaintext, block after block.

The principle fits in one sentence: in CBC mode, each plaintext block depends on the previous ciphertext block via a XOR. If the server tells us—even indirectly—whether the decrypted padding is correct, we can manipulate that previous block to guess each plaintext byte.

The oracle

An oracle is any observable behavior that separates valid padding from invalid: a different HTTP status, an error message, or simply the response time. Here, a 500 error on broken padding and a 200 otherwise. Enough.

// the key idea We target two blocks: C[i-1] (which we'll forge) and C[i] (which we want to decrypt). By playing with C[i-1], we force the last decrypted byte to equal 0x01 (a valid one-byte padding). At that point, a XOR gives us the intermediate byte, hence the plaintext.

Byte-by-byte attack

We brute-force the last byte of the forged block (256 tries max) until we get valid padding, then work our way up to 0x02 0x02, 0x03 0x03 0x03, and so on. Skeleton:

def decrypt_block(c_prev, c_target):
    inter = bytearray(16)
    for pad in range(1, 17):
        forged = bytearray(16)
        for k in range(1, pad):
            forged[-k] = inter[-k] ^ pad
        for guess in range(256):
            forged[-pad] = guess
            if oracle(bytes(forged) + c_target):
                inter[-pad] = guess ^ pad
                break
    return bytes(a ^ b for a, b in zip(inter, c_prev))

We repeat over every block, sliding the window. The IV plays the role of "previous block" for the very first block.

! padding false positive Rare but real: a byte can produce a valid 0x02 0x02 by chance when we were aiming for 0x01. We resolve the ambiguity by modifying the second-to-last byte and rechecking. Without that guard, the attack derails on certain blocks.
flag captured flag{cbc_p4dd1ng_l34ks_0n3_byt3_4t_4_t1m3}

Fix it

  • Use an authenticated mode (AES-GCM, or encrypt-then-MAC). A MAC verified before decryption silences the oracle.
  • Never leak the cause of a decryption failure—even a timing difference is enough.
  • Reject early, uniformly, without distinguishing padding vs integrity vs format.
Encryption without authentication is a lock with no strike plate: it looks shut, but it pushes open.