Ya explicamos anteriormente en el post Json Web Tokens (JWT) Qué son y como funcionan qué son los Json Web Token, su estructura y uso, así que en el caso de hoy vamos a ver que tipos de ataques existen sobre este tipo de tokens y, sobre todo, como podemos evitar ser víctimas de los mismos si desarrollamos una aplicación que los utilice.
Como ya se comentó en la primera parte, para falsificar un token, se debe disponer de las claves correctas, pero si la configuración de JWT no se implementa correctamente, hay muchas formas de eludir los controles y poder modificar el token a nuestro antojo para conseguir un acceso no autorizado.
Índice
Ataques básicos
En primer lugar vamos a ver algunos fallos básicos que permiten que sea posible explotar una vulnerabilidad en la implementación de JWT.
Permitir que no se utilice un algoritmo
El estándar JWT acepta muchos tipos de algoritmos para generar una firma como:
- RSA
- HMAC
- Curva elíptica
- Ninguno
El uso del valor None cuando se especifica un algoritmo, indica que el token no está firmado. Si el algoritmo está permitido, es posible omitir la verificación de la firma modificando un algoritmo None existente y eliminando la firma.
Vamos a verlo con el siguiente ejemplo:
1 2 3 4 5 6 7 8 9 |
{ "alg": "HS256", "typ": "JWT" }. { "name": "John Doe", "user_name": "john.doe", "is_admin": false }.SIGNATURE |
Codificado y firmado el token se vería así (la firma sería la tercera línea):
1 2 3 |
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. eyJuYW1lIjoiSm9obiBEb2UiLCJ1c2VyX25hbWUiOiJqb2huLmRvZSIsImlzX2FkbWluIjpmYWxzZX0. fSppjHFaqlNcpK1Q8VudRD84YIuhqFfA67XkLam0_aY |
Si el valor None está permitido, un atacante podría simplemente usarlo para reemplazar el algoritmo utilizado y deshacerse de la firma:
1 2 3 4 5 6 7 8 9 |
{ "alg": "None", "typ": "JWT" }. { "name": "John Doe", "user_name": "john.doe", "is_admin": true }. |
Aunque el token no está firmado, vemos como es capaz de generar el token modificado
1 2 |
eyJhbGciOiJOb25lIiwidHlwIjoiSldUIn0. eyJuYW1lIjoiSm9obiBEb2UiLCJ1c2VyX25hbWUiOiJqb2huLmRvZSIsImlzX2FkbWluIjp0cnVlfQ. |
Para evitar esto, es importante no aceptar tokens cuyo algoritmo sea None, none, NONE o cualquier otra variación de mayúsculas y minúsculas en el encabezado alg.
No verificar la firma
Muchas bibliotecas JWT propocionan un método para la decodificación y otro para la verificación del token, por ejemplo estas dos funciones:
- decode() -> sólo decodifica el token de la codificación base64url sin verificar la firma
- verify() -> decodifica el token y verifica la firma
En ocasiones, los desarrolladores pueden mezclar estos dos métodos, de tal forma que la firma nunca se verifica y la aplicación acepta cualquier token.
Los desarrolladores también pueden deshabilitar la verificación de firmas por ser un entorno de desarrollo y luego olvidarse en el paso a producción, o incluso que sea un cambio temporal que se acabe quedando, lo que podría dar lugar a un acceso arbitrario, con su consiguiente acceso a una cuenta o a una escalada de privilegios.
Supongamos que tenemos el siguiente token válido
1 2 3 4 5 6 7 8 9 |
{ "alg": "HS256", "typ": "JWT" }. { "name": "John Doe", "user_name": "john.doe", "is_admin": false } |
Como la firma del mismo no se verifica, un atacante podría modificar el mismo para cambiar los permisos del usuario y enviar el siguiente token
1 2 3 4 5 6 7 8 9 |
{ "alg": "HS256", "typ": "JWT" }. { "name": "John Doe", "user_name": "john.doe", "is_admin": true } |
Al no hacer la verificación del mismo, sería posible que modificase el valor, en este caso, del campo is_admin, consiguiendo con ello escalar privilegios a un usuario administrador.
Confusión de algoritmo
JWT acepta algoritmos de cifrado simétrico y asimétrico. Según el tipo de cifrado utilizado, se debe utilizar un secreto compartido, o un par de claves público/privadas.
Veámoslo en la siguiente tabla:
Algoritmo | Clave utilizada para firmar | Clave utilizada para verificar |
Asimétrica (RSA) | Clave privada | Clave pública |
Simétrica (HMAC) | Secreto compartido | Secreto compartido |
Cuando una aplicación utiliza un cifrado asimétrico, puede publicar de forma abierta su clave pública, debido a que mantiene en secreto su clave privada, lo que permite a la aplicación firmar tokens con dicha clave privada y que cualquier pueda verificar el token con la clave pública. La vulnerabilidad de confusión de algoritmos surge cuando una aplicación no verifica si el algoritmo del token recibido coincide con el algoritmo esperado.
En muchas bibliotecas JWT, el método para realizar dicha verificación es:
- verify(token, secret) -> si el token está firmado con HMAC
- verify(token, pubkey) -> si el token está firmado con RSA o similar
Por desgracia, en algunas bibliotecas, este método por si solo no verifica si el token recibido está firmado por el algoritmo esperado por la aplicación, por lo que en el caso de HMAC, este método tratará el segundo argumento como un secreto compartido, y en el caso de RSA como una clave pública.
Si la clave pública dentro de la aplicación es accesible, un atacante podría falsificar el token de la siguiente forma:
- Se cambia el algoritmo del token a HMAC
- Se manipula el payload para obtener el resultado deseado
- Se firma el token malicioso con la clave pública encontrada en la aplicación
- Se envía el token JWT a la aplicación
La aplicación esperará el cifrado RSA, por lo que cuando un atacante proporciona HMAC en su lugar, el método verify() tratará la clave pública como un secreto compartido de HMAC y utilizará cifrado simétrico en lugar de asimétrico. Esto provocará que el token se firmará con la clave pública no secreta de la aplicación y que posteriormente se verifique con la misma clave pública.
Para evitar esta vulnerabilidad, las aplicaciones deben de verificar que el algoritmo del token recibido es el que se espera antes de verificar el mismo.
Descifrar la clave secreta
Con el cifrado simétrico, una firma criptográfica es tan sólida como el secreto utilizado. Si una aplicación utiliza un secreto débil, un atacante podría simplemente aplicar fuerza bruta para tratar de descubrir el mismo. También es posible obtener el secreto utilizado por medio de otras vulnerabilidades en el portal como pueden ser LFI, XXE, SSRF, etc.
Para este propósito se puede utilizar la extensión de BurpSuite llamada JWT HeartBreaker.
Para evitar este tipo de vulnerabilidades, es importante utilizar secretos bastante robustos y evitar que pueda ser obtenido el mismo por medio de otras vulnerabilidades, ya que esta exposición podría comprometer todo el mecanismo de seguridad de la aplicación.
Ataques avanzados
Verificación con archivos arbitrarios
El encabezado Key ID (kid) es un encabezado opcional que tiene un tipo de cadena utilizado para indicar la clave específica presente en el sistema de archivos o una base de datos, par aluego utilizar su contenido para verificar la firma.
Este parámetro es útil si la aplicación dispone de varias claves para firmar los tokens, pero puede ser muy peligroso si el mismo es inyectable, ya que un atacante podría apuntar a un archivo específico cuyo contenido es predecible.
Vamos a verlo con un ejemplo:
1 2 3 4 5 6 7 8 9 10 |
{ "alg": "HS256", "typ": "JWT", "kid": "key1" }. { "name": "John Doe", "user_name": "john.doe", "is_admin": false } |
En este caso, un atacante podría intentar insertar /dev/null como fuente de clave para obligar a la aplicación a usar una clave vacía, debido a que el contenido del mismo es predecible:
1 2 3 4 5 6 7 8 9 10 |
{ "alg": "HS256", "typ": "JWT", "kid": "../../../../../../dev/null" }. { "name": "John Doe", "user_name": "john.doe", "is_admin": true } |
Si el recorrido del directorio indicado tiene éxito, el atacante podría firmar un token malicioso utilizando una cadena vacía. Esta misma técnica puede utilizarse con archivos estáticos conocidos, como por ejemplo, ficheros js o css de la aplicación.
Para evitar este y otros ataques de inyección similares, las aplicaciones siempre deben de sanear el valor del parámetro kid antes de su uso.
Ejecución de comandos por parámetros inyectables
Siguiendo con la explicación anterior del parámetro kid, si el mismo es vulnerable a la inyección de comandos, podría modificarse para conducir a una ejecución remota de código (RCE), como vemos en el siguiente ejemplo:
1 2 3 4 5 6 7 8 9 10 |
{ "alg": "HS256", "typ": "JWT", "kid": "key1|/usr/bin/cat /etc/passwd" }. { "name": "John Doe", "user_name": "john.doe", "is_admin": false } |
Como se aprecia en el anterior token, es posible realizar un RCE, y obtener por ejemplo acceso a ficheros sensibles del servidor, o incluso conseguir acceso a la misma
Para evitar este y otros ataques de inyección similares, las aplicaciones siempre deben de sanear el valor del parámetro kid antes de su uso.
Inyección SQL mediante parámetros inyectables
Siguiendo también con el parámetro kid, en este caso si la finalidad es recuperar una clave de una base de datos, podría ser el parámetro vulnerable a la inyección SQL. Si el ataque tiene éxito, un atacante podría controlar el valor devuelto al parámetro kid desde una consulta SQL y utilizarlo para firmar un token malicioso.
Volviendo al mismo ejemplo, supongamos que una aplicación utiliza la siguiente consulta para obtener la clave JWT a través del parámetro kid:
1 |
SELECT value FROM keys WHERE key='key1' |
Un atacante podría realizar una inyección UNION en el parámetro kid para controlar el valor de la clave
1 2 3 4 5 6 7 8 9 10 |
{ "alg": "HS256", "typ": "JWT", "kid": "xxxx' UNION SELECT 'field1" }. { "name": "John Doe", "user_name": "john.doe", "is_admin": true } |
Si la inyección anterior tiene éxito, la consulta real que ejecutará la aplicación sería la siguiente
1 |
SELECT key FROM keys WHERE key='xxxx' UNION SELECT 'field1' |
Esta consulta devolverá field1 al parámetro kid, lo que permitirá al atacante firmar el token malicioso simplemente con field1.
Para evitar este y otros ataques de inyección similares, las aplicaciones siempre deben de sanear el valor del parámetro kid antes de su uso.
Ataques mediante el encabezado jku
Además del parámetro kid visto anteriormente, también es posible utilizar el parámetro jku para especificar la URL del conjunto de claves JWT. Este parámetro indica donde puede encontrar la aplicación la clave JWK que se utilizará para verificar la firma, en otras palabras, indicará la clave pública en formato JSON.
Vamos a verlo mejor con un ejemplo:
1 2 3 4 5 6 7 8 9 10 |
{ "alg": "RS256", "typ": "JWT", "jku":"https://example.com/key.json" }. { "name": "John Doe", "user_name": "john.doe", "is_admin": false } |
El archivo key.json especificado podría parecerse a
1 2 3 4 5 |
{ "kty": "RSA", "e": "AQAB", "n": "izDUv02gYatku5SyIG-Vds5roqhUm7DIRmsfLV1CukdrxG38t_Lz1d.............JN18yH-" } |
La aplicación verificará la firma utilizando la clave web Json obtenida a través del valor del encabezado jku, como se aprecia en la siguiente imagen:
Ahora para el atacante, este puede cambiar el valor del parámetro jku para que apunte a su propia máquina en lugar de a la original, o si ha podido subir su fichero de claves, a otro fichero dentro del mismo servidor. Si este es aceptado, el atacante podría firmar sus propios token utilizando su propia clave privada. Una vez enviado, la aplicación buscará el token del atacante, en lugar del generado por la misma aplicación:
Para evitar estos ataques, las aplicaciones suelen utilizar un filtrado de URL, aunque siempre hay formas en las que podemos eludir dichos filtrados, por ejemplo:
- El uso de cadenas específicas en la URL, obligando a que la firma incluya un cadena específica en la dirección del token
- El uso de fragmentos de URL con el carácter #
- El uso de la jerarquía de nombre de DNS específicos
- Encadenado con redirecciones abiertas, inyecciones de cabecera, SSRF, etc
Por estas razones, es muy importante que la aplicación incluya whitelists de hosts permitidos y tenga un filtrado correcto de URLs permitidas. Más allá de esto, la aplicación no deberá de tener otras vulnerabilidades que permitan que un atacante pueda encadenar fallos para obtener eludir los filtrados realizados.
Y hasta aquí todo por ahora, espero que les ayude a poder identificar este tipo de fallos en sus aplicaciones, y a poder hacer de internet un poco más seguro.
Y como siempre, cualquier duda, comentario o sugerencia es bienvenida.