Skip to main content

Uso del bucle de eventos de Node.js para ataques de temporización

Escrito por

16 de febrero de 2016

0 minutos de lectura

Hace poco más de 3 años, un grupo de amigos y yo formamos un grupo llamado pasten para participar en la competencia de Chaos Computer Club Capture The Flag (CTF). Es una CTF estilo Jeopardy, en la que los equipos participantes deben resolver desafíos relacionados con la seguridad en distintas categorías, como explotación, ingeniería inversa, web, análisis forense y criptografía. Las CTF son divertidas y educativas, y sin duda recomiendo participar. ¡Son aún más divertidas cuando ganas, como nosotros este año!

La competencia de este año incluyó, entre otros desafíos, una aplicación de Node.js vulnerable a un interesante ataque de temporización que aprovechaba el bucle de eventos propio de Node.js. En esta publicación, repasamos el desafío, explicamos el riesgo y mostramos cómo un atacante podría descubrir y explotar una vulnerabilidad de este tipo. Esperamos que también te ayude a evitar vulnerabilidades similares en tu propio código.

Ataques de temporización

Antes de entrar en el desafío, expliquemos qué es un ataque de temporización.

Imagina un servicio que, cuando escribes una contraseña incorrecta para un correo electrónico existente, responde: «La quinta letra de tu contraseña es incorrecta, inténtalo de nuevo». Suena absurdo, ¿verdad? Dar esa información permite que un atacante pruebe la contraseña por fuerza bruta, un carácter a la vez, y hace que entrar sea muy fácil. Sin embargo, eso es exactamente lo que ocurre cuando usamos una comparación de cadenas ingenua para verificar contraseñas o tokens de autenticación.

La comparación nativa de cadenas, incluido el operador == de JavaScript, suele recorrer las dos cadenas (de igual longitud), comparar los caracteres uno por uno y detenerse cuando encuentra uno distinto. Así que, si comparo foo con bar, el bucle se ejecuta una vez; en cambio, al comparar foo con fox, compara 3 caracteres y tarda más en terminar.

Ejemplo de función de autenticación vulnerable:

function isAuthenticated(user, token) {
  var correctToken = FetchUserTokenFromDB(user);
  return token === correctToken;
}

Comparar un solo carácter es bastante rápido, pero aun así tarda lo suficiente. Las investigaciones muestran que un atacante puede medir eventos con una precisión de entre 15 y 100 µs a través de Internet, y con una precisión de 100 ns en una red local. Los atacantes pueden usar estas técnicas, y esos pequeños retrasos pueden revelar casi lo mismo que decirle al usuario qué carácter estaba mal.

Este tipo de ataque se llama ataque de temporización y puede realizarse siempre que la entrada afecte el tiempo de procesamiento. Para evitarlo, debemos hacer que la comparación de cadenas tarde lo mismo independientemente de la contraseña ingresada; por ejemplo, podemos aplicar xoring a las dos contraseñas y comprobar si el resultado es cero.

No vulnerable a ataques de temporización:

var mismatch = 0;
for (var i = 0; i < a.length; ++i) {
  mismatch |= (a.charCodeAt(i) ^ b.charCodeAt(i));
}
return mismatch;

El desafío: Sequence Hunt

Con esta información, entremos en el desafío. Todo comenzó con un enlace a una página que mostraba lo siguiente:

Sequence Hunt¡Te damos la bienvenida a mi búsqueda de secuencias de enteros! Debes calcular cinco números entre 0 y 100 implementando los algoritmos que aparecen abajo. Cuando creas que tienes la solución correcta, envíalos como parámetros GET aquí.También puedes obtener información sobre ti aquí.Espero mucho tráfico, así que diseñé esto en torno a bucles de eventos de E/S asíncrona.¡Diviértete!

[EDITAR]Alguien me envió esto. Como cada verificación tarda bastante, ahora el servidor espera hasta que hayan transcurrido tres segundos desde que llega la solicitud para evitar estos ataques.[/EDITAR]

AlgoritmosPENDIENTE publicar algoritmos

El texto aquí apunta a /check, donde debemos enviar los parámetros correctos para obtener la flag, y IOLoops lleva a /info, que muestra lo siguiente:

eres 37.120.106.140cantidad de solicitudes en tu IOLoop: 1

¿Qué sabemos hasta ahora?

  1. Hay un algoritmo desconocido que recibe 5 números del 0 al 100.

  2. Para obtener la flag, debemos proporcionar los números correctos.

  3. Es probable que el tiempo de ejecución del algoritmo dependa de la entrada, lo que podría permitir un ataque de temporización.

  4. El servidor siempre espera al menos 3 segundos antes de enviar la respuesta para dificultar los ataques de temporización.

Ahora debemos descubrir cómo capturar la flag. Como el desafío se llamaba Sequence Hunt, usaré sequencehunt.com como dominio de ejemplo, pero ¡no envíes ataques a este dominio (que no tiene relación con el desafío)! Puedes ejecutar la aplicación de ejemplo vulnerable que escribí. Debería comportarse de forma similar a la de la CTF.

Exploración

Una comprobación rápida confirma que una solicitud check tarda unos 3 segundos en responder, independientemente de los valores que enviemos. Esto significa que no podemos probar todas las combinaciones por fuerza bruta. Habría que probar 100^5 (10 mil millones) de combinaciones, lo que tomaría miles de años…

$ curl "https://sequencehunt.com/check?val0=1&val1=1&val2=1&val3=1&val4=1"
you are 37.120.106.140
At least one value is wrong!

$ time curl "https://sequencehunt.com/check?val0=1&val1=1&val2=1&val3=1&val4=1"
you are 37.120.106.140
At least one value is wrong!
0.00s user
0.01s system
0% cpu
3.174 total

Al explorar la página /info, observamos que cada solicitud a /check incrementa el contador de IOLoop, y lo mismo ocurre con la propia solicitud a /info.

$ curl "https://sequencehunt.com/info"
you are 37.120.106.140<br>request count on your IOLoop: 1

$ curl "https://sequencehunt.com/info"
you are 37.120.106.140<br>request count on your IOLoop: 2

$ curl "https://sequencehunt.com/info"
you are 37.120.106.140<br>request count on your IOLoop: 3

$ curl "https://sequencehunt.com/check?val0=1&val1=1&val2=1&val3=1&val4=1"
you are 37.120.106.140<br>At least one value is wrong!

$ curl "https://sequencehunt.com/info"
you are 37.120.106.140<br>request count on your IOLoop: 5

Enviar solicitudes a /check con distintos números aleatorios no produce diferencias de tiempo estadísticamente significativas. Al parecer, el algoritmo tarda menos de 3 segundos, por lo que todas las solicitudes simplemente responden después del mínimo especificado de 3 segundos.

Sin embargo, notamos que una solicitud a /info tarda bastante más en responder si hay una solicitud /check en curso; esto nos da una pista relacionada con la temporización que podemos explorar. Para entender el siguiente paso, hablemos un momento sobre el bucle de eventos de Node.

Bucle de eventos de Node.js

Diseñado pensando en la escalabilidad, Node.js (y JavaScript en general) es un entorno asíncrono y basado en eventos. Cuando una parte del código necesita realizar una acción que podría bloquear la ejecución, como abrir un archivo o escribir en la red, registra una función de devolución de llamada, inicia la acción correspondiente y termina. Cuando la acción finaliza (por ejemplo, se abre el archivo), el bucle de eventos ejecuta el evento de devolución de llamada.

El modelo basado en eventos es extremadamente escalable, ya que los hilos nunca se quedan esperando, pero introduce un problema cuando una función tarda mucho en completarse. Como el servidor Node.js ejecuta un solo hilo por núcleo (de manera predeterminada), una función de larga duración ocuparía todo el núcleo mientras las demás esperan su turno en la cola. En el navegador, esto explica el mensaje de error «la secuencia de comandos tarda demasiado en ejecutarse» que quizás hayas visto. En el servidor, simplemente hará que las solicitudes se queden pendientes.

Para entender mejor el bucle de eventos de Node.js, consulta la excelente charla que Philip Roberts dio en JSConf o lee uno de estos artículos.

Cómo resolver el enigma

En nuestro caso, el bucle de eventos nos proporcionó la información de temporización que necesitábamos. Mientras /check estaba en ejecución, el bucle de eventos no recuperaba el control y la solicitud /info quedaba pendiente. En cambio, la función setTimeout simplemente registra un evento programado y cede el control, lo que permite procesar rápidamente las solicitudes a /info. Subí a GitHub el código de un servidor sencillo que muestra este efecto, por si quieres probarlo localmente.

Ahora que teníamos una forma de obtener información de temporización (lo que también se conoce como canal lateral), solo debíamos enviar una solicitud a /check y, sin esperar la respuesta, enviar una solicitud a /info y medir cuánto tardaba en regresar.

Diagrama de secuencia que muestra a un atacante midiendo el tiempo de las solicitudes de verificación y sus respuestas mientras el bucle de eventos del servidor está bloqueado y setTimeout retrasa la respuesta.

Para automatizar la búsqueda y compensar las variaciones de la red, el siguiente paso fue escribir un script de Python que, dado un conjunto de entradas:

  • Abre n hilos

  • Envía una solicitud check y mide cuánto tarda en responder una solicitud paralela a /info

  • Calcula el promedio de los tiempos y devuelve el resultado

Usé n=5 y fue suficiente. El valor podría ser mayor según la calidad de la conexión a Internet y las características de temporización del algoritmo. Estos son los resultados de la primera ejecución; observa la diferencia significativa cuando el valor es 4.

[0, 0, 0, 0, 0]: 0.4933500289916992
[1, 1, 1, 1, 1]: 0.3603970050811768
[2, 2, 2, 2, 2]: 0.4297104358673095
[3, 3, 3, 3, 3]: 0.4705570697784423
[4, 4, 4, 4, 4]: 0.9154952526092529 <<<<<<<<
[5, 5, 5, 5, 5]: 0.4637355804443359
[6, 6, 6, 6, 6]: 0.3557830333709716
[7, 7, 7, 7, 7]: 0.4418128013610840
[8, 8, 8, 8, 8]: 0.4297045707702637

Como la diferencia de tiempo solo aparecía cuando 4 ocupaba la primera posición, el siguiente paso fue probar todas las opciones para los demás parámetros. Encontramos que el segundo dígito era 12.

[4, 10, 10, 10, 10]: 0.840841388702392
[4, 11, 11, 11, 11]: 0.859339499473571
[4, 12, 12, 12, 12]: 1.228700304031372 <<<<<<<<
[4, 13, 13, 13, 13]: 0.907831811904907

Y así sucesivamente, hasta obtener todos los parámetros correctos: [4, 12, 77, 98, 35]

$ curl 'https://sequencehunt.com/check?val0=4&val1=12&val2=77&val3=98&val4=35'
you are 37.120.106.140
Congratulations, here is your prize:
32C3_round_and_round_and_round_IT_goes

Flag capturada.

Resumen

Los ataques de temporización son una amenaza real y afectan tanto a empresas pequeñas como grandes (como Google). Es posible detectar incluso diferencias pequeñas en el tiempo de ejecución con una cantidad relativamente pequeña de muestras (a escala de Internet) y usarlas para obtener información y perfeccionar los ataques por fuerza bruta. En Node.js, en particular, el bucle de eventos puede servir como otra señal para realizar ataques de temporización.

Algunos consejos para proteger tu aplicación de este tipo de fallas:

Por último, si quieres demostrar tus habilidades y enfrentarte a estos desafíos, ¡prueba participar en la competencia Capture The Flag de CCC el próximo año!

Empieza con los desafíos de Capture the Flag

Aprende a resolver desafíos de captura la bandera con nuestro taller virtual introductorio a pedido.

Leer más

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.

feature insights context
Blog

Los ataques autónomos ya están aquí. La defensa debe estar a su altura.

Los atacantes autónomos están reduciendo el tiempo disponible para defenderse. Descubre cómo el descubrimiento, la corrección, la validación y la prevención continuos pueden ayudar a los equipos de seguridad a seguirles el ritmo.

Blog

Tu backlog de vulnerabilidades ya no es deuda técnica: es una superficie de ataque

Un backlog de vulnerabilidades en crecimiento es más que deuda técnica: es una superficie de ataque. Descubre por qué las suposiciones obsoletas sobre el riesgo, los atacantes automatizados y los hallazgos encadenados requieren un nuevo enfoque.