Padding Oracle en detalle
En criptografía, el ataque de padding oracle consiste en utilizar la validación del padding de un mensaje cifrado para descifrarlo. Con este ataque, además de descifrar, también es posible llegar a cifrar un mensaje sin conocer la clave de cifrado.
El factor clave para determinar si una aplicación es vulnerable a este ataque radica en como se realiza la validación de un mensaje cifrado. Para identificar la vulnerabilidad necesitamos que la aplicación devuelva un mensaje de error especifico que nos indique que se trata de un error en el padding, o que seamos capaces de diferenciar de alguna forma cuando el padding es correcto y cuando es incorrecto.
Para entender como funciona la vulnerabilidad de padding oracle debemos conocer como funciona AES, el modo de cifrado en cadena CBC y como se realiza el relleno o padding.
AES y CBC
Si estas leyendo esto, ya deberías saber que AES es un algoritmo de cifrado simétrico por bloques. En el modo de operación CBC (Cipher block chaining). a cada bloque de texto se le realiza la operación XOR con el bloque anterior ya cifrado. Además, se utiliza un vector de inicialización (IV) en el primer bloque.
De forma análoga, en el proceso de descifrado se realiza la operación XOR tras descifrar cada bloque.
Padding
El padding o relleno se basa en completar el último bloque antes de realizar el cifrado, en AES se utiliza PKCS#7, completando el último bloque con bytes del mismo valor al número de bytes de relleno. Por ejemplo, si para completar el bloque se necesitan 5 bytes, el relleno sería 0505050505
. El padding es obligatorio, si el último bloque no necesita relleno, se añade un bloque entero de relleno, para un bloque de 128 bits (16 bytes) se utilizaría 0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f
.
Identificando un parámetro vulnerable
Esta vulnerabilidad se suele encontrar en el descifrado de algún elemento introducido por el usuario, un token, una cookie, etc. Así que lo primero que debemos identificar es un parámetro que este potencialmente cifrado con AES, como puede ser un valor en hexadecimal o base64. AES utiliza un tamaño de bloque de 128 bits, lo que significa que los datos deben ser múltiples de 16 bytes, o 32 caracteres en hexadecimal.
Por ejemplo el siguiente texto en hexadecimal de 64 caracteres corresponde a 2 bloques cifrados con AES.
20f78bc84c2df7cdd77ea7732ea04420a8beeafefac8f31ff70b7a84ce52bd7c
En este ejemplo tenemos un JSON cifrado con AES y codificado en base64. al revertir la codificación en base64 obtenemos 160 bytes (320 caracteres en hexadecimal), lo que corresponde a 10 bloques.
Si modificamos la longitud del parámetro, la aplicación nos devuelve un error indicando que la entrada debe tener una longitud múltiple de 16 bytes.
Identificando un error de padding
En el caso de que la aplicación nos devuelva un error descriptivo indicando que el error se debe al padding podemos dar por confirmada la existencia de la vulnerabilidad. Para obtener un error de padding, debemos modificar un byte del último bloque o el último byte del penúltimo bloque.
Por ejemplo, esta aplicación devuelve el error “PaddingException” al modificar un parámetro cifrado con AES.
En cambio, si el padding es correcto obtenemos un resultado correcto, u otro tipo de mensajes de error relacionados con el contenido del mensaje, pero no con la función de descifrado.
Descifrar un byte
Para verificar que podemos obtener un error de padding de forma voluntaria, debemos modificar el mensaje cifrado de forma que el último byte del mensaje descifrado termine con el padding 01
. Para ello, modificamos el último byte del texto cifrado del bloque anterior, dado que no conocemos el contenido del mensaje deberemos realizar 256 intentos, o solo 16 si este byte ya es un byte de padding con un valor entre 00
y 0f
. En el caso de no encontrar “otro” resultado con padding correcto, supondremos que el último byte es un byte de padding con el valor 01
y repetiremos el proceso con el penúltimo byte.
Pongamos como ejemplo el siguiente texto cifrado con 5 bytes de padding.
Al modificar el último byte del penúltimo bloque obtenemos un último bloque con padding válido 01
, aunque de esta forma se corrompe el plaintext del penúltimo bloque. Además, podemos conocer cual era el byte sin cifrar utilizando la operación XOR: 0x20 ⊕ 0x24 ⊕ 0x01 = 0x05
Descifrar los datos de un bloque
Una vez hemos obtenido el último byte del mensaje, repetimos el proceso para el resto de bytes del bloque.
Para el siguiente byte realizamos la iteración buscando el padding 0202
, como ya hemos obtenido el último byte, calculamos con XOR que ciphertext debemos introducir: 0x20 ⊕ 0x05 ⊕ 0x02 = 0x27. Por tanto, solo realizaremos 256 iteraciones con el penúltimo byte hasta encontrar el resultado válido.
Continuamos el descifrado del resto de bytes automatizando el proceso.
Descifrar el resto de bloques
Como hemos visto en los ejemplos anteriores, para cada bloque a descifrar, necesitamos también el bloque anterior.Esto implica que no podemos descifrar el primer bloque de la secuencia. Dependiendo de la implementación realizada en la aplicación, el primer bloque suele contener el vector de inicialización (IV), aunque también puede haber un IV prefijado en el código.
Generar un bloque cifrado
Por la forma que se construye la cadena de bloques, si conocemos el plaintext del primer bloque, podemos alterar el IV para generar un bloque cifrado: NewIV = IV ⊕ Plaintext ⊕ NewPlaintext
Usando este método solo podemos modificar un único bloque con un máximo de 15 bytes de datos y uno de padding.
En este ejemplo procedemos a modificar el parámetro ID del JSON calculando un nuevo IV.
Generar varios bloques cifrados
Combinando las técnicas anteriores, podemos generar un texto cifrado que por su tamaño necesite varios bloques. Utilizaremos padding oracle para descifrar los bloques y XOR para modificar estos bloques por el contenido deseado.
Para explicar el procedimiento, usaremos el siguiente ejemplo para “cifrar” el JSON {"id":"1","key":"RYxUB!nb6e2NKkHpkdPZHA~~"}
- Descifrar un bloque, por ejemplo 00000000000000000000000000000000, o utilizar un bloque del que conozcamos su plaintext y ciphertext.
- Hacemos XOR del bloque descifrado con el ultimo bloque en claro que deseamos inyectar, el resultado se utilizará como ciphertext del penúltimo bloque.
- Repetimos los pasos anteriores hasta obtener el primer bloque, que se corresponde con el IV. De igual forma, desciframos cada bloque, hacemos XOR con el texto en claro deseado y el resultado se utiliza como ciphertext del bloque anterior.
Automatizando la extracción
Podemos automatizar la extracción de datos con el script https://github.com/mpgn/Padding-oracle-attack/
Obtener el valor de un IV hardcodeado
En algunas aplicaciones, el vector de inicialización (IV) esta fijado en el código y no se muestra al usuario. Cuando esto ocurre, no podemos descifrar el primer bloque. Para descubrir el IV necesitamos conseguir el plaintext del primer bloque y de esta forma utilizar la función XOR.
En el siguiente ejemplo, tenemos el bloque cifrado 6159c28d3019a62eb96180dced387011
.
Para descifrarlo realizamos el ataque de padding oracle con los bloques 00000000000000000000000000000000 6159c28d3019a62eb96180dced387011
obteniendo el bloque descifrado 6dffba1163080ffc7c84f24f350d8634
.
Si conocemos que el texto cifrado es “PATATASFRITAS”, realizamos la operación XOR y obtenemos el IV.
Al ser el IV fijo, a partir de este momento podemos descifrar el primer bloque de cualquier mensaje.
Reto para practicar
En el reto Encrypted Pastebin del CTF de hackerone se pueden probar las tecnicas explicadas en este articulo. Para acceder es necesario estar registrado en hackerone.