Padding Oracle Attack

Cryptosystem

AES in CBC mode.

Requirements

Encrypted messages using block ciphers in CBC mode. A padding scheme like PKCS#7 or PKCS#1 v1.5. An oracle that reveals whether the padding of a decrypted ciphertext is valid or not.

Description

The Padding Oracle Attack exploits a side-channel where an attacker can repeatedly send modified ciphertexts and observe whether a padding error occurs. This leakage allows the attacker to gradually decrypt the ciphertext without knowing the key.

The attack takes advantage of how padding is validated after decryption. If an oracle (like a server) tells you whether the padding is correct or not, you can learn information about the plaintext byte-by-byte.

This attack is powerful and has affected real-world protocols like SSL/TLS, ASP.NET, and Java crypto APIs.

Example

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from Crypto.Random import get_random_bytes

BLOCK_SIZE = 16

# Victim setup
key = get_random_bytes(BLOCK_SIZE)

def encrypt(plaintext: bytes) -> tuple[bytes, bytes]:
    iv = get_random_bytes(BLOCK_SIZE)
    cipher = AES.new(key, AES.MODE_CBC, iv)
    ciphertext = cipher.encrypt(pad(plaintext, BLOCK_SIZE))
    return iv, ciphertext

def padding_oracle(iv: bytes, ciphertext: bytes) -> bool:
    try:
        cipher = AES.new(key, AES.MODE_CBC, iv)
        unpad(cipher.decrypt(ciphertext), BLOCK_SIZE)
        return True
    except ValueError:
        return False

# Attacker
def decrypt_block(prev_block: bytes, target_block: bytes) -> bytes:
    recovered = bytearray(BLOCK_SIZE)
    intermediate = bytearray(BLOCK_SIZE)
    for i in range(1, BLOCK_SIZE + 1):
        pad_byte = i
        for guess in range(256):
            prefix = bytearray(BLOCK_SIZE)
            for j in range(1, i):
                prefix[-j] = intermediate[-j] ^ pad_byte
            prefix[-i] = guess
            if padding_oracle(bytes(prefix), target_block):
                intermediate[-i] = guess ^ pad_byte
                recovered[-i] = intermediate[-i] ^ prev_block[-i]
                break
    return bytes(recovered)

def full_decrypt(iv: bytes, ciphertext: bytes) -> bytes:
    blocks = [iv] + [ciphertext[i:i+BLOCK_SIZE] for i in range(0, len(ciphertext), BLOCK_SIZE)]
    plaintext = b""
    for i in range(1, len(blocks)):
        decrypted = decrypt_block(blocks[i-1], blocks[i])
        plaintext += decrypted
    return unpad(plaintext, BLOCK_SIZE)

# Simulate attack
plaintext = b"Top secret message, please don't leak me."
iv, ciphertext = encrypt(plaintext)
recovered_plaintext = full_decrypt(iv, ciphertext)

print(f"Original plaintext : {plaintext}")
print(f"Recovered plaintext: {recovered_plaintext}")

Real world

  • In 2010, Padding Oracle Attacks were used in the "Padding Oracle On Downgraded Legacy Encryption" (POODLE) attack on SSL 3.0.
  • The ROBOT Attack The ROBOT attack exploited a padding oracle vulnerability in the RSA encryption scheme used in TLS, allowing attackers to decrypt sensitive data.