Manipulación de tipos: cómo escapar de los entornos aislados de plantillas
21 de marzo de 2017
0 minutos de lecturaUna característica clave de los lenguajes interpretados, como JavaScript y Ruby, es el tipado dinámico, en el que los tipos de las variables se determinan y actualizan durante la ejecución. El tipado dinámico tiene sus desventajas, pero puede hacer que el software sea más flexible y acelerar el desarrollo. Lamentablemente, también abre la puerta a un vector de ataque llamado manipulación de tipos, en el que los atacantes intentan modificar el tipo de una variable y provocar un comportamiento inesperado.
Esta es la primera de una serie de publicaciones sobre la manipulación de tipos. En cada una se mostrarán una o más vulnerabilidades reales que se pueden explotar mediante la manipulación de tipos y se explicará cómo podrían haberse evitado. El uso de ejemplos reales busca tanto alertarte sobre estas vulnerabilidades específicas en paquetes que quizá utilices como ayudarte a aprender de estos errores y evitarlos en tu propio código.
En esta publicación, nos centraremos en usar la manipulación de tipos para eludir los entornos aislados de los frameworks de plantillas. Estas plantillas suelen contar con mecanismos de protección integrados, pero, como pronto veremos, su protección nunca es perfecta. Nuestros ejemplos se basan en vulnerabilidades de las plataformas Dust.js de LinkedIn y Nunjucks de Mozilla, con una breve mención de Angular.
Vulnerabilidad de ejecución remota de comandos en LinkedIn Dust.js
Nuestro ejemplo principal será una vulnerabilidad en Dust.js, un popular framework de plantillas publicado y utilizado por LinkedIn. También se sabe que PayPal utiliza Dust.js. Allí, un investigador descubrió una vulnerabilidad de ejecución remota de comandos que se explotaba mediante la manipulación de tipos.
La vulnerabilidad se divulgó el 9 de enero de 2015 y se corrigió en la versión 2.6.0, publicada el 14 de septiembre de 2016. Si utilizas Dust.js, asegúrate de comprobar si tus aplicaciones usan la versión vulnerable.
Dependencia explícita del tipo
Como la mayoría de las bibliotecas de plantillas, Dust.js admite condiciones en sus plantillas, como se muestra en el siguiente ejemplo, que usa el parámetro device para elegir qué HTML renderizar:
Luego, estas condiciones se evalúan mediante la función eval(), una forma sencilla de admitir condiciones muy complejas en la plantilla. Este es un fragmento de la función if de Dust.js, donde se ve cómo se combinan los params proporcionados por el usuario y el cond estático proporcionado por el desarrollador para crear el valor que se evaluará:
Como la condición puede contener datos introducidos por el usuario (por ejemplo, el parámetro device), Dust.js usa una función de saneamiento para evitar que se evalúen como código valores maliciosos de los parámetros. Este es un fragmento de esa función:
El código de saneamiento es claro y sólido, y contempla todos los caracteres que se sabe que permiten salir de la cadena entre comillas. Además, se observa que la llamada HCHARS.test(s) solo se ejecuta con cadenas, probablemente para evitar errores con valores indefinidos.
Aunque esto puede evitar errores, esta comprobación de tipo es precisamente lo que abre la puerta a un ataque de manipulación de tipos. Si el atacante logra forzar que s sea un arreglo (o cualquier objeto que no sea una cadena), puede eludir por completo la comprobación. Más adelante en el código, el arreglo se convertiría implícitamente en una cadena al pasar la condición a eval, lo que permitiría ejecutar código JavaScript de forma remota.
De cadena a arreglo
El siguiente paso para un atacante es intentar manipular el tipo de la variable. En aplicaciones basadas en API, los atacantes pueden intentar cambiar el tipo modificando un payload JSON, como vimos en el caso de la vulnerabilidad de Buffer de Mongoose. Sin embargo, como se trata de una plataforma web de plantillas, nos centraremos en manipular el tipo mediante qs.
El paquete qs es el paquete de JavaScript más utilizado para analizar cadenas de consulta y se incluye de forma predeterminada en express, request y otros paquetes populares. qs convierte una cadena de consulta en un objeto de JavaScript para facilitar su uso. Estos son algunos usos típicos de qs:
Como quizá hayas notado, qs determina el tipo del objeto creado según cómo aparece en la cadena de consulta. Si el mismo nombre aparece varias veces, se representa como un arreglo. De manera similar, si un parámetro se indica explícitamente como arreglo, ese será su tipo al analizarlo.
Desde aquí, queda poco para activar la vulnerabilidad de Dust.js. Con la plantilla de ejemplo anterior, que se utilizaba en PayPal, todo lo que debe hacer un atacante es proporcionar dos veces el argumento device o incluirlo como un parámetro device[]=value. Así, la variable se pasará como un arreglo y eludirá la lógica de saneamiento.
Explotación posterior de Dust.js
Aunque se aleja un poco del tema de la manipulación de tipos, resulta interesante ver cómo un atacante puede aprovechar esta vulnerabilidad mediante las acciones que también se conocen como «explotación posterior».
Primero, el atacante debe lograr que se ejecute código. Puede hacerlo proporcionando una URL como esta:
Registrar un mensaje en la consola no es muy útil para un atacante, así que podría proporcionar un script más elaborado, por ejemplo, para enviar información. En el caso de la explotación de PayPal, el investigador utilizó un payload como este:
Esto se traduce en una llamada a eval como esta:
La explotación posterior usa el módulo child_process para ejecutar localmente un comando curl y enviar el archivo /etc/passwd al atacante. Con un payload un poco más largo, también se podría haber hecho con un comando nativo de Node, pero el uso de child_process eleva la vulnerabilidad de ejecución remota de código JS a ejecución remota de comandos de shell, lo que amplía las opciones del atacante.
Opciones de corrección
La mayoría de los ataques de manipulación de tipos (y este no es la excepción) se pueden resolver rechazando, normalizando o gestionando de forma personalizada el tipo de variable. Estas tres opciones se pueden combinar para establecer varias capas de defensa.
Rechazar significa admitir solo tipos específicos. En este caso, Dust.js podría haber optado por rechazar los parámetros que no fueran cadenas en sus plantillas. Esto reduciría la funcionalidad, por lo que su conveniencia depende de cómo se utilice este framework.
Normalizar significa convertir varios tipos de entrada en un solo tipo. En este caso, Dust.js convertía implícitamente las variables en cuestión en cadenas al pasarlas a la función eval(). En lugar de convertirlas en cadenas en ese punto, las funciones anteriores podrían convertir en cadenas los arreglos, enteros u otros valores recibidos y procesar el resto del código —incluido el saneamiento— sabiendo que solo se admiten cadenas.
Por último, gestionar de forma personalizada significa escribir una lógica específica para todos los tipos permitidos. En este caso, Dust.js podría haber identificado que se trataba de un arreglo y normalizado cada carácter, teniendo en cuenta también las formas peligrosas en que se pueden combinar varios elementos del arreglo. Esta es la opción más frágil de las tres, pero también admite entradas más complejas.
Dust.js optó por una forma de gestión personalizada que busca detectar todos los objetos que tienen una función toString. Esta solución aborda el problema, pero deja cierta fragilidad en el código, ya que el código posterior podría convertir un arreglo en una salida para el usuario de otra manera. Este es el fragmento pertinente del parche de Dust.js:
Si utilizas una versión vulnerable de Dust.js y no puedes actualizar por algún motivo, puedes normalizar los tipos de las entradas que se pasan a Dust.js en tu código o rechazar los tipos que no sean cadenas para bloquear los arreglos maliciosos. Puedes comprobar si usas una versión vulnerable con la CLI de Snyk o la integración con GitHub.
Vulnerabilidad XSS en Mozilla Nunjucks
La vulnerabilidad de Dust.js no es única, sino que responde a un patrón que se repite. Por ejemplo, veamos una oportunidad similar para manipular tipos que existió en otro framework de plantillas: la biblioteca Nunjucks de Mozilla. Matt Austin divulgó esta vulnerabilidad el 6 de septiembre de 2016, y se corrigió en la versión 2.4.3, publicada el 9 de septiembre de 2016. Al igual que con Dust.js, puedes usar Snyk para comprobar si tus aplicaciones utilizan una versión vulnerable de Nunjucks.
Al igual que Handlebars, Mustache y otros, Nunjucks te permite indicar nombres de variables entre dos llaves (``) para señalar que el valor debe codificarse como HTML. Este es un ejemplo de esta protección en acción:
Sin embargo, al igual que Dust.js, la función de saneamiento de Nunjucks solo escapaba los parámetros de tipo cadena. Este es un fragmento del código de saneamiento:
Siguiendo una lógica similar a la anterior, una URL como esta (analizada por qs):
Dará lugar a una llamada y a una salida XSS como esta:
Como puedes ver, el problema sigue el mismo patrón: no se anticipa la capacidad del atacante para manipular el tipo. Como resultado, un atacante puede eludir el saneamiento e inyectar código malicioso.
Para corregir el problema, al igual que en Dust.js, Nunjucks podría rechazar las entradas de tipo arreglo, normalizarlas como cadenas o gestionar de forma personalizada el saneamiento de los arreglos. Como muestra este parche, Mozilla optó por normalizar las entradas de tipo arreglo con el método toString.
Resumen
La manipulación de tipos es un vector de ataque menos conocido, pero representa un peligro muy real para todos los lenguajes de tipado dinámico. En estos lenguajes, debemos acostumbrarnos a considerar el tipo como otra forma de entrada y permitir solo los valores de una lista de tipos autorizados, normalizar cada variante o gestionarla de forma personalizada.
En esta publicación, vimos cómo se podía usar la manipulación de tipos para escapar del entorno aislado de un framework de plantillas, un problema que afectó a muchos frameworks. Crear un entorno aislado sin controlar el entorno de ejecución es extremadamente difícil, por lo que Angular decidió abandonarlo por completo.
En próximas publicaciones mostraremos otras situaciones en las que puede entrar en juego la manipulación de tipos, para ayudarte a comprender mejor el riesgo que representa y cómo puede manifestarse.


