Cuando el remedio es peor que la enfermedad (limites absurdos en protección DoS)

Kuko Armas <kuko@canarytek.com>
|

A veces un sistema de protección ante ataques de denegación de servicio (DoS), si se configura de manera demasiado agresiva, puede provocar un efecto peor del que provocaría el ataque DoS del que se supone que debe protegernos. Este es un claro ejemplo…

Actualizacion 24 Marzo 2020

Cuando reportamos el error al Mozilla, nos contestaron que no podía ser un error del navegador, que en todo caso seria del sistema operativo. A pesar de decirles que solo fallaba a partir de una version determinada de Firefox, y con otros navegadores no pasaba, decidieron cerrar el Bug y marcarlo como “INVALID”.

Pues bien, casi 2 años despues, el 21 de Marzo de 2020 se han dado cuenta de que efectivamente parece un problema del navegador, y han reabierto el bug, asociandolo al nuevo reporte: Bug 1622859

El problema

Hace algunas semanas un cliente más o menos grande (una administración local) nos llamó porque estaban teniendo serios problemas de rendimiento con determinada aplicación web de administración electrónica. Al parecer, llevaban algún tiempo utilizándola sin problemas, pero en los últimos días los usuarios habían empezado a quejarse de que iba muy lenta y les daba muchos errores de conexión.

Aparentemente solo tenían estos problemas de “lentitud” con esa aplicación, el acceso a otras aplicaciones, y en general a cualquier página web, les funcionaba bien.

El diagnóstico

Primera fase: Diagnóstico inicial

Evidentemente nuestra primera sospecha fue que era un problema de rendimiento en los servidores del proveedor de la aplicación, pero en la opciones de desarrollador del navegador pudimos ver que la mayor parte del tiempo que tardaba en cargar la página se invertía en establecer la conexión TCP.

El segundo paso (también evidente) fue capturar el tráfico de red para ver lo que estaba pasando. Lo que vimos fue algo parecido a esto:

a.a.a.a -> b.b.b.b TCP 51367 > https [SYN]
a.a.a.a -> b.b.b.b TCP [TCP Retransmission] 51367 > https [SYN]
a.a.a.a -> b.b.b.b TCP [TCP Retransmission] 51367 > https [SYN]
b.b.b.b -> a.a.a.a TCP https > 51367 [SYN,ACK]
a.a.a.a -> b.b.b.b TCP 51367 > https [ACK]

Es decir, el servidor estaba ignorando los primeros paquetes de inicio de conexión TCP (SYN) y por eso había un retardo muy alto en el establecimiento de la conexión. Incluso muchos de ellos nunca recibían respuesta SYN+ACK por parte del servidor, lo que provocaba muchos errores de timeout.

La captura anterior se realizo en el interfaz exterior de nuestro firewall, por lo que quedaba descartado que se tratara de algún tipo de filtro en nuestra infraestructura.

A la vista de estos resultados, nuestro diagnóstico inicial fue que algo en el lado del proveedor estaba limitando la tasa de conexiones desde nuestra IP Además, observamos que si el tráfico lo enviábamos desde otra IP de origen (tenemos varias IP públicas en ese firewall), el problema se resolvía durante un rato, por lo que estaba claro que se trataba de una limitación por dirección de origen.

Nuestra recomendación fue que realizáramos una captura en ambos extremos de la conexión para determinar si era una limitación del proveedor o algo “por el camino”.

Segunda fase: El misterio del SYN perdido

Nunca entenderé la razón por la que el proveedor tardó casi una semana en acceder a capturar tráfico en su extremo de la conexión, con el consiguiente “cabreo” de los usuarios. De hecho pensamos que ya habían descubierto que era problema suyo y no querían admitirlo…

Pero cuando hicimos la captura en ambos extremos este fue el resultado:

  • Captura en nuestro extremo
a.a.a.a -> b.b.b.b TCP 51367 > https [SYN]
a.a.a.a -> b.b.b.b TCP [TCP Retransmission] 51367 > https [SYN]
a.a.a.a -> b.b.b.b TCP [TCP Retransmission] 51367 > https [SYN]
b.b.b.b -> a.a.a.a TCP https > 51367 [SYN,ACK]
a.a.a.a -> b.b.b.b TCP 51367 > https [ACK]
  • Captura en el extremo del proveedor
a.a.a.a -> b.b.b.b TCP 51367 > https [SYN]
b.b.b.b -> a.a.a.a TCP https > 51367 [SYN,ACK]
a.a.a.a -> b.b.b.b TCP 51367 > https [ACK]

Es decir, el proveedor solo recibía uno de nuestros paquetes de inicio de conexión (SYN), al que contestaba con el SYN+ACK. Por tanto, los retardos no eran culpa del proveedor, algo en la red estaba descartando nuestros primeros TCP SYN.

Para ampliar información, realizamos pruebas de envío de paquetes SYN al puerto 443 con hping3 y descubrimos que algo filtraba los SYN a partir de los 625 paquetes, y que tardaban muchísimo en liberarse. Es decir, una vez enviados los 625 paquetes desde una IP concreta, desde esa IP no se volvía a recibir respuesta a ningún SYN hasta pasados muchos minutos.

Además, cuando enviábamos paquetes SYN a cualquier puerto, siempre recibíamos la respuesta SYN+ACK, aunque luego no pudiéramos conectar a dicho puerto. Es decir, todo parecía indicar que había algún tipo de SYN Proxy que contestaba a cualquier SYN, independientemente de si en la maquina de destino real ese puerto estaba abierto o no.

Puesto que el proveedor en cuestión tiene alojadas sus infraestructuras en un famoso datacenter de un famoso proveedor español (que empieza por T y rima con armónica ;), les recomendamos que escalaran la incidencia a dicho proveedor (que llamaremos simplemente “T”)

Tercera fase: La protección DoS

Cuando conseguimos hacer la prueba a tres bandas (nosotros, el proveedor y personal de T), el técnico de T nos confirmó que tenían dispositivos que actuaba de SYN Proxy para “proteger” la entrada al datacenter frente a ataques DoS. Nos confirmó que el problema que estábamos viendo era porque el dispositivo estaba detectando un “ataque DoS” desde nuestras IP, y que por eso se descartaban muchos paquetes SYN.

Al borrar nuestra IP de la lista de IP “atacantes” de dicho dispositivo, todo nos funcionaba correctamente durante un rato, pero el dispositivo detectaba muchas conexiones no completadas y activaba las medidas “defensivas”, con lo que en menos de media hora volvíamos a tener el mismo problema. Pregunté en cuanto tiempo había que llegar a las 625 conexiones no completadas para que lo consideraran un “ataque”, porque no es lo mismo 625 conexiones no completadas en 1 minuto que en 1 hora. Lo primero es claramente un ataque, lo segundo puede ser accidental y difícilmente supondría un problema para el servidor. Aun estoy esperando la respuesta a esa pregunta…

En conclusión, el dispositivo de protección anti DoS nos estaba haciendo un DoS porque detectaba un supuesto “ataque” desde nuestra IP, aunque nadie nos ha aclarado aun los parámetros para considerarlo un ataque DoS

Cuarta fase: La causa de nuestro “ataque” DoS

El siguiente paso era diagnosticar por qué dicho dispositivo detectaba un ataque SYN Flooding desde nuestra IP (que fuentes sin confirmar nos dijeron que son dispositivos Corero)

Para diagnosticarlo volvimos a hacer capturas de tráfico en ambos extremos, pero esta vez después de que el técnico de T borrara nuestra IP de la lista de “atacantes”, para no sufrir los descartes de paquetes SYN. Esta vez lo que llamaba la atención es que veíamos muchas retransmisiones de las respuestas SYN+ACK del servidor:

a.a.a.a → b.b.b.b TCP 59819 → 443 [SYN]
b.b.b.b → a.a.a.a TCP 443 → 59819 [SYN, ACK]
b.b.b.b → a.a.a.a TCP [TCP Retransmission] 443 → 59819 [SYN, ACK]
b.b.b.b → a.a.a.a TCP [TCP Retransmission] 443 → 59819 [SYN, ACK]
b.b.b.b → a.a.a.a TCP [TCP Retransmission] 443 → 59819 [SYN, ACK]

Es decir, generábamos el paquete de inicio de conexión SYN, el servidor nos respondía con un SYN+ACK, pero nunca enviábamos el ACK de establecimiento. Esa era la razón por la que parecía un ataque SYN Flooding (aunque esto pasaba en un porcentaje bastante bajo de las conexiones)

Las posibles causas que se me ocurrían que podían producir que no se enviara el ACK de establecimiento de la conexión eran:

  1. Porque los checksums TCP de las respuestas no fueran correctos. Por lo que vimos en las capturas, los checksums eran correctos, por lo que no parecía que esta fuera la causa
  2. Porque nuestro firewall los estuviera descartando por haber caducado la entrada en la tabla NAT y lo considerara una nueva conexión. Tampoco parecía que ese fuera el caso, porque el timeout que tenemos en entradas SYN_SENT es de 120 segundos y las retransmisiones nos llegaban unos 10 segundos después del primer SYN.
  3. Porque la máquina que inició la conexión descartara la respuesta SYN+ACK porque no le gustaran las opciones TCP de la misma. Tampoco parecía que fuera ese el caso, porque las opciones TCP en respuestas ignoradas eran exactamente las mismas que las de otras respuestas que sí se tenían en cuenta en cuenta y a las que se enviaba el ACK final
  4. Porque la máquina origen de la conexión ya no estuviera interesada en abrir esa conexión, y por eso ignoraba la respuesta. Esta opción es la que nos parecía a priori la mas probable porque en los casos en los que se observaban retransmisiones de la respuesta SYN+ACK, no había retransmisiones del SYN inicial. Si la máquina descartara la respuesta SYN+ACK por considerarla inválida, debería retransmitir el SYN de apertura de conexión, porque desde su punto de vista no había recibido un SYN+ACK válido. El hecho de no retransmitir el SYN indica que ya no estaba interesada en abrir esa conexión

Para confirmar si efectivamente el problema era el comentado en el punto 4, capturamos tráfico en una de las máquinas clientes, y vimos que efectivamente se iniciaba el proceso de apertura de muchas conexiones, y luego se ignoraban algunas. Por ejemplo:

a.a.a.a → b.b.b.b TCP 59013 → 443 [SYN]
a.a.a.a → b.b.b.b TCP 59014 → 443 [SYN]
a.a.a.a → b.b.b.b TCP 59015 → 443 [SYN]
a.a.a.a → b.b.b.b TCP 59016 → 443 [SYN]
a.a.a.a → b.b.b.b TCP 59017 → 443 [SYN]
a.a.a.a → b.b.b.b TCP 59018 → 443 [SYN]
a.a.a.a → b.b.b.b TCP 59019 → 443 [SYN]
a.a.a.a → b.b.b.b TCP 59020 → 443 [SYN]
a.a.a.a → b.b.b.b TCP 59021 → 443 [SYN]
a.a.a.a → b.b.b.b TCP 59022 → 443 [SYN]
b.b.b.b → a.a.a.a  TCP 443 → 59013 [SYN, ACK]
b.b.b.b → a.a.a.a  TCP 443 → 59014 [SYN, ACK]
b.b.b.b → a.a.a.a  TCP 443 → 59015 [SYN, ACK]
b.b.b.b → a.a.a.a  TCP 443 → 59016 [SYN, ACK]
b.b.b.b → a.a.a.a  TCP 443 → 59017 [SYN, ACK]
b.b.b.b → a.a.a.a  TCP 443 → 59018 [SYN, ACK]
a.a.a.a → b.b.b.b TCP 59018 → 443 [ACK]
b.b.b.b → a.a.a.a  TCP 443 → 59019 [SYN, ACK]
b.b.b.b → a.a.a.a  TCP 443 → 59020 [SYN, ACK]
b.b.b.b → a.a.a.a  TCP 443 → 59021 [SYN, ACK]
a.a.a.a → b.b.b.b TCP 59019 → 443 [ACK]
a.a.a.a → b.b.b.b TCP 59020 → 443 [ACK]
a.a.a.a → b.b.b.b TCP 59021 → 443 [ACK]
b.b.b.b → a.a.a.a  TCP 443 → 59022 [SYN, ACK]
a.a.a.a → b.b.b.b TCP 59022 → 443 [ACK]
b.b.b.b → a.a.a.a  TCP [TCP Retransmission] 443 → 59017 [SYN, ACK]
b.b.b.b → a.a.a.a  TCP [TCP Retransmission] 443 → 59015 [SYN, ACK]
b.b.b.b → a.a.a.a  TCP [TCP Retransmission] 443 → 59013 [SYN, ACK]
b.b.b.b → a.a.a.a  TCP [TCP Retransmission] 443 → 59014 [SYN, ACK]
b.b.b.b → a.a.a.a  TCP [TCP Retransmission] 443 → 59016 [SYN, ACK]
b.b.b.b → a.a.a.a  TCP [TCP Retransmission] 443 → 59015 [SYN, ACK]
b.b.b.b → a.a.a.a  TCP [TCP Retransmission] 443 → 59017 [SYN, ACK]
b.b.b.b → a.a.a.a  TCP [TCP Retransmission] 443 → 59013 [SYN, ACK]
b.b.b.b → a.a.a.a  TCP [TCP Retransmission] 443 → 59014 [SYN, ACK]
b.b.b.b → a.a.a.a  TCP [TCP Retransmission] 443 → 59016 [SYN, ACK]
b.b.b.b → a.a.a.a  TCP [TCP Retransmission] 443 → 59017 [SYN, ACK]
b.b.b.b → a.a.a.a  TCP [TCP Retransmission] 443 → 59013 [SYN, ACK]
b.b.b.b → a.a.a.a  TCP [TCP Retransmission] 443 → 59015 [SYN, ACK]
b.b.b.b → a.a.a.a  TCP [TCP Retransmission] 443 → 59014 [SYN, ACK]
b.b.b.b → a.a.a.a  TCP [TCP Retransmission] 443 → 59016 [SYN, ACK]

Es decir, nuestro cliente a.a.a.a iniciaba 10 conexiones al servidor b.b.b.b desde los puertos 59013 al 59022, recibía el SYN+ACK de las 10 conexiones, pero solo completaba (enviaba el ACK) 5 de ellas (puertos 59018 a 59022), lo que producía retransmisiones del SYN+ACK a los puertos 59013 al 59017 desde el servidor. El hecho de que no se retransmita el SYN inicial desde esos 5 puertos indica que la máquina cliente ya no está interesada en abrir esas 5 conexiones, y por eso descarta los SYN+ACK

Puesto que esto pasaba con una aplicación web, el “culpable” era, o bien el navegador, o bien algún componente que gestionara directamente la apertura de conexiones (Javascript?)

En cualquier caso, este comportamiento no debería pasar de ser un “problema menor”, porque la tasa de retrasmisiones de SYN+ACK no es tan alta y esos paquetes no contienen datos, por lo que el impacto en consumo de ancho de banda es mínimo. Lo que realmente suponía un problema es que este comportamiento se interpretara como un posible ataque de SYN Flooding y empezaran a descartarse conexiones lo que suponía un verdadero DoS

Tras varias pruebas se pudo determinar que este comportamiento solo pasaba con determinadas versiones de Firefox (concretamente la 59.0.2), con Google Chrome o con versiones anteriores de Firefox no pasa. Hemos reportado el bug a mozilla

Conclusión

En este caso hemos visto que un pequeño bug en un navegador, que no debería tener mayores consecuencias y que probablemente nunca hubiéramos detectado por no tener un impacto real en rendimiento, se ha visto “amplificado” por un sistema de protección ante ataque DoS de SYN Flooding con una configuración excesivamente agresiva (en nuestra opinión). El verdadero ataque DoS lo ha producido el sistema de “protección DoS” sobre el cliente, que simplemente generaba muchas conexiones no completadas debido a un bug de un navegador y a que se conectan muchos usuarios desde la misma IP. A pesar de que el porcentaje de conexiones TCP no completadas con respecto a las completadas no es muy alto, por lo que esta claro que es algo accidental

De hecho, con la configuración actual es más sencillo hacer un ataque DoS por SYN Flooding que si esta protección no existiera. Porque basta con generar unos pocos cientos de conexiones TCP con IP falsa para que desde esa IP no puedan usar dicha aplicación. Si, en este caso el DoS es al cliente, no al servidor, pero el perfil de este servidor es que tiene pocos clientes grandes, ya que sus clientes son administraciones públicas. Y si evitas que los clientes legítimos puedan usar una aplicación, es lo mismo que “atacar” al servidor.

Por tanto, este es un claro ejemplo de ese dicho del saber popular: es peor el remedio que la enfermedad