Explotación de Buffer
5 de abril de 2016
0 minutos de lecturaEntre las maravillas de Node se esconde una bomba de tiempo llamada Buffer. Si se maneja incorrectamente, esta clase riesgosa puede filtrar fácilmente memoria del servidor y, con ella, tus secretos y claves, debido a una vulnerabilidad divulgada por Feross Aboukhadijeh y Mathias Buss Madsen. Proyectos populares y de buena reputación han caído en esta trampa, incluidos los paquetes npm mongoose, ws, request y, más recientemente, sequelize, que presentaron vulnerabilidades de amplio alcance.
En esta publicación explicaremos cómo funciona Buffer y por qué se comporta así. Ejecutaremos un exploit contra una aplicación vulnerable para mostrar mejor sus consecuencias. Por último, hablaremos de los cambios que puedes esperar en Node 6 y enumeraremos 5 pasos que puedes seguir para protegerte del comportamiento predeterminado de Buffer, y
¿Cómo llegamos hasta aquí?
JavaScript del lado del cliente nos evita tener que gestionar la asignación de memoria. Como ocurre con muchos lenguajes similares, en JS el motor subyacente (por ejemplo, V8) asigna memoria y la libera mediante el recolector de basura según sea necesario, lo que hace que programar sea más sencillo y seguro. En el navegador, impedir el acceso a la memoria también es necesario para mantener el entorno aislado en el que se ejecuta JS.
Cuando JS se extendió al servidor con Node, se eliminó el entorno aislado del navegador y aumentó la necesidad de procesar datos binarios de forma fácil y rápida. Para satisfacer estas necesidades, Node introdujo la clase Buffer, que trabaja con datos binarios. Ten en cuenta que más adelante, ES6 creó clases adicionales orientadas a datos binarios, especialmente TypedArray y ArrayBuffer.
Buffer es una matriz mutable de datos binarios que se puede inicializar con una cadena, una matriz o un número. Estos son algunos fragmentos de la documentación de node.js:
Las dos primeras variantes simplemente crean una representación binaria del valor que reciben. La última, en cambio, preasigna un búfer del tamaño especificado, lo que la convierte en un búfer útil, sobre todo al leer datos de un flujo.
La falla de seguridad
Al usar el constructor number de Buffer, se asigna la memoria, pero no se llena con ceros. En cambio, el búfer asignado contiene… lo que sea que estuviera en la memoria en ese momento. Puedes comprobarlo tú mismo ejecutando Node en una terminal y creando repetidamente un Buffer de un tamaño determinado; verás que cada uno contiene valores diferentes, ya que apunta a otra sección de memoria utilizada anteriormente.
Este comportamiento está bien documentado, y puedes poner en cero la memoria asignada simplemente llamando a «buf.fill(0)». Sin embargo, si olvidas hacerlo, podrías filtrar memoria. Exponer memoria quizá no parezca tan grave hasta que consideras la cantidad de secretos —claves, código fuente e información del sistema— que podrían quedar expuestos. Heartbleed, la enorme vulnerabilidad de OpenSSL de 2014, permitía exponer memoria.
Demostración del riesgo de seguridad
Para entender mejor la vulnerabilidad de Buffer, veamos una aplicación vulnerable llamada Goof. Su código está publicado en GitHub, junto con instrucciones de instalación por si quieres ejecutar estos exploits tú mismo.

Esta es una aplicación sencilla (y vulnerable) de lista de tareas pendientes. Usa mongoose para comunicarse con MongoDB, donde se almacenan los elementos, mediante un esquema sencillo:
Como puedes ver, el campo content, que contiene la tarea pendiente, es de tipo Buffer, lo que permite que admita datos binarios. Como muchas aplicaciones de Node.js, snyk-demo-todo también expone una API JSON, que usaremos para crear un elemento desde la línea de comandos:
El valor de la variable content se pasó al constructor Buffer, que inicializó una cadena corta que luego se escribió en la base de datos. El nuevo elemento se puede ver al explorar la aplicación y también se devuelve (codificado en base64) en la respuesta:

Ahora veamos el exploit de la vulnerabilidad. Enviaremos la misma solicitud, pero reemplazaremos la cadena "Buy Milk" por el número 800 sin comillas:
Como antes, el valor de content se pasa al constructor Buffer. Sin embargo, esta vez el valor es de tipo number, lo que activa el otro constructor de Buffer… Esto inicializará el elemento de la lista de tareas con 800 bytes de memoria sin inicializar, que se almacenan en la base de datos. Estos datos volverán a aparecer en la respuesta HTML (aunque son binarios y difíciles de leer) y se devolverán a nuestro curl tras codificarlos en base64:

Ahora podemos repetir esta llamada una y otra vez para obtener más fragmentos de memoria y decodificarlos desde base64. Es probable que eliminemos las notas creadas a medida que avanzamos para evitar sospechas, mientras revisamos los datos o buscamos patrones como secret, ssh-key, código fuente y más.
¿Cómo lo solucionamos?
En nuestra aplicación de ejemplo, la vulnerabilidad estaba en la dependencia mongoose. La solución fue convertir los números en matrices y tratarlos como un número individual en lugar de como una longitud. Si usas una versión vulnerable de mongoose, actualízala a una versión más reciente. Si no tienes claro si la usas, utiliza Snyk para encontrar y corregir esta y otras vulnerabilidades.
Si la vulnerabilidad está en tu propio código, puedes impedir el uso del constructor number (convirtiéndolo opcionalmente en una matriz o una cadena) o usar la función fill(0) cada vez que se llame al constructor. Ten en cuenta que, aunque poner la memoria en cero es eficiente, lleva algo de tiempo en el caso de los búferes grandes, así que considera el impacto que esto podría tener en el rendimiento de tu aplicación.
Próximos cambios en Node 6
Este comportamiento predeterminado de Buffer es preocupante y ha causado múltiples vulnerabilidades conocidas, pero cambiarlo en las versiones actuales de Node es difícil, ya que podría romper aplicaciones. Sin embargo, a partir de Node 6, se recomienda usar los métodos explícitos alloc y allocUnsafe, que asignan espacio con y sin ponerlo en cero, respectivamente. Esta API explícita puede ayudar a los desarrolladores a evitar errores similares.
El constructor predeterminado seguirá disponible y funcionará igual, aunque quedará obsoleto y no se recomendará su uso. Dicho esto, puedes usar la opción --zero-fill-buffers de Node (también a partir de Node 6) para que el constructor numérico predeterminado de Buffer ponga en cero el búfer asignado.
5 pasos para mantenerte a salvo
Buffer es una clase útil, pero peligrosa. Si estás pensando en usarla, ten en cuenta lo siguiente:
¿Puedes usar
TypedArrayoArrayBufferen su lugar? Estas clases también son compatibles con datos binarios y ponen en cero la memoria.¿Puedes impedir el uso del constructor
number? Si es así, hazlo, como hizomongoose.¿Puedes poner en cero los datos asignados con
buf.fill(0)? Esto tendrá un pequeño impacto en el rendimiento, pero será más seguro.Si de verdad no puedes hacer todo lo anterior, controla con mucho cuidado adónde va el contenido de
Buffer. Con muchísimo cuidado.Si usas paquetes npm como dependencias, ejecuta snyk wizard para corregir las vulnerabilidades relacionadas con
Bufferque puedan tener tus dependencias.
Empieza con los retos de Capture the Flag
Aprende a resolver retos de Capture the Flag viendo nuestro taller virtual de nivel básico a pedido.


