intermediatecat/crypto~3 min read

CBC Padding Oracle Attacks

Decrypt AES/DES ciphertexts and forge arbitrary encrypted messages by exploiting PKCS#7 padding validation errors in CBC mode.

What is a Padding Oracle Attack?

In block cipher encryption operating in Cipher Block Chaining (CBC) mode, plaintext is divided into fixed block sizes (e.g., 16 bytes for AES). If the final plaintext block is shorter than the block size, padding (typically PKCS#7) is appended.

A Padding Oracle exists when a server decrypts a ciphertext and reveals (either explicitly via an error message or implicitly via HTTP status codes / timing) whether the decrypted plaintext has valid PKCS#7 padding.

Attacker ---> [ Modifies Ciphertext Block C_{i-1} ] ---> Server Decrypts Block C_i
                                                              |
Attacker <--- [ Returns "Padding Valid" / "Padding Invalid" ] -+

PKCS#7 Padding Rules

For a block size of 16 bytes:

  • If 1 byte of padding is needed: ... 01
  • If 2 bytes are needed: ... 02 02
  • If 3 bytes are needed: ... 03 03 03
  • If a full block is padded (16 bytes): 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10

Decryption Attack Algorithm

During CBC decryption:

$$P_i = D_k(C_i) \oplus C_{i-1}$$

Where $P_i$ is the plaintext block, $C_i$ is the target ciphertext block, $C_{i-1}$ is the preceding ciphertext block (or IV for the first block), and $D_k$ is the block decryption function.

By modifying the last byte of $C_{i-1}$ (let’s call it $C’{i-1}[15]$) and sending $(C’{i-1}, C_i)$ to the oracle:

  1. Iterate $C’_{i-1}[15]$ from 0x00 to 0xFF.
  2. When the oracle responds with “Padding Valid”, the last byte of the intermediate decrypted block $I_i[15]$ satisfies: $$I_i[15] \oplus C’{i-1}[15] = 0x01 \implies I_i[15] = C’{i-1}[15] \oplus 0x01$$
  3. Reconstruct the original plaintext byte: $$P_i[15] = I_i[15] \oplus C_{i-1}[15]$$
  4. Repeat this byte by byte from right to left (updating forced padding values to 0x02, 0x03, etc.) to decrypt the full block without knowing the AES key!

Automated Exploit with Python / PyCryptodome

import requests
from pwn import log

# Example padding oracle exploit loop template
TARGET_URL = "http://challenge.ctf/decrypt"

def check_padding(ciphertext_hex):
    res = requests.get(f"{TARGET_URL}?token={ciphertext_hex}")
    return "Invalid padding" not in res.text

def decrypt_block(prev_block, target_block):
    intermediate = bytearray(16)
    plaintext = bytearray(16)
    modified_prev = bytearray(16)
    
    for byte_pos in range(15, -1, -1):
        padding_val = 16 - byte_pos
        for i in range(byte_pos + 1, 16):
            modified_prev[i] = intermediate[i] ^ padding_val
            
        found = False
        for candidate in range(256):
            modified_prev[byte_pos] = candidate
            test_ct = (modified_prev + target_block).hex()
            if check_padding(test_ct):
                intermediate[byte_pos] = candidate ^ padding_val
                plaintext[byte_pos] = intermediate[byte_pos] ^ prev_block[byte_pos]
                found = True
                break
        if not found:
            raise Exception(f"Failed to find byte at position {byte_pos}")
            
    return bytes(plaintext)

Automated Tools

PadBuster

padbuster http://challenge.ctf/index.php?token=ENC_HEX ENC_HEX 16 -encoding 0

Poracle (Python framework)

pip install poracle