
Información sobre Kubernetes CVE-2020-8558, incluyendo exploit de prueba de concepto.
CVE-2020-8558 es una vulnerabilidad de Kubernetes que fue publicada porque kube-proxy inesperadamente hace que los servicios del host vinculados a localhost estén disponibles para otros en la red. Pongo énfasis en inesperadamente porque esta vulnerabilidad se debe a un defecto de diseño (descuido), no a un fallo de implementación (error). El código hace exactamente lo que dice, pero todos nosotros no logramos reconocer las implicaciones de seguridad de esa decisión.
Para permitir que los procesos del host accedan a los servicios NodePort a través de la dirección 127.0.0.1 (localhost), kube-proxy establece la configuración sysctl net.ipv4.conf.all.route_localnet=1. Según la documentación del kernel, esta configuración hace que el kernel "no considere las direcciones de loopback como marcianas" — una consecuencia de lo cual es que podrían ser accedidas por otros nodos en la red. ¡Eso es un gran problema si tienes servicios sensibles no autenticados cuya única protección es estar vinculados a localhost!
Al momento de escribir esto, la comunidad de Kubernetes todavía está trabajando en la mejor manera de abordar CVE-2020-8558. Las dos opciones obvias son dejar de establecer el sysctl route_localnet en primer lugar, o bloquear los paquetes localnet enrutados inapropiadamente usando iptables. Una corrección que utiliza esta última estrategia ya ha sido lanzada en kubelet >= 1.18.4, 1.17.7 o 1.16.11. También puedes aplicarla tú mismo consultando el issue de Kubernetes para esta CVE, enlazado abajo.
¿Por qué merece una ID de CVE establecer net.ipv4.conf.all.route_localnet=1? Principalmente porque viola nuestra intuición sobre las redes IP.
Desde al menos el RFC 1122 de 1989, los paquetes de la red localhost 127.0.0.1/8 han sido tratados de manera especial, prohibidos de aparecer "fuera de un host". (Contáctame en Twitter si conoces una referencia anterior a las propiedades especiales de 127/8.) Cualquier host compatible con RFC esencialmente tiene una regla de firewall implícita e inamovible que bloquea el acceso externo a servicios vinculados a 127.0.0.1 (y otras IPs en esa red — ¡intenta hacer ping a 127.127.127.127 si nunca lo has hecho!). Hemos llegado a depender y esperar ese comportamiento. Frecuentemente ejecutamos servicios sensibles sin autenticación ni cifrado, y los vinculamos a localhost por seguridad. Por ejemplo, backends HTTP en texto plano, el almacén de clave-valor redis, y el puerto inseguro vestigial del api-server de Kubernetes suelen estar protegidos de intrusiones de esta manera. Estamos tan acostumbrados a este comportamiento que es una parte arraigada de nuestro sentido intuitivo de lo que significa ser un host IP. Visto de esta manera, es comprensible que numerosos expertos hayan pasado por alto esta falla durante tanto tiempo.
¿Cómo funciona?
Llamemos "nodo" a cualquier entidad con una dirección IP. Los paquetes IP se envían de un nodo a otro, identificados por las direcciones IP de origen y destino en el encabezado del paquete. Cada nodo IP es o un enrutador (llamado gateway en RFC1122) o un host. La diferencia principal es que cuando un host recibe paquetes destinados a la dirección de otro, los ignora. Un enrutador consulta su tabla de enrutamiento y retransmite (reenvía) los paquetes en un intento de acercarlos a su destino final. Un host conocerá algunos nodos conectados localmente; para acceder a otros nodos, debe enviar sus paquetes a un enrutador conectado localmente. Esas conexiones locales pueden ser punto a punto (como un enlace PPP o algunas redes virtuales) o de medio compartido (como Ethernet).
Tu buzón de correo puede conceptualizarse como un enlace punto a punto entre tu casa y la oficina de correos local. Para enrutar un paquete a través de un enlace punto a punto, el host solo necesita aplicar la dirección de destino correcta y transmitir el paquete. (Esto ocurre en la Capa 3 del modelo OSI.) Para enrutar un paquete a través de un enlace de medio compartido, el host primero debe construir un circuito virtual punto a punto a través del medio compartido. En redes Ethernet/IP, esto se hace mediante ARP, en la capa 2 del modelo OSI. Esencialmente, si puedes transmitir un paquete ARP, puedes decirle a otro host "Oye, estoy aquí" y te creerá. (Cuando esto se hace de manera inapropiada, se llama envenenamiento de caché ARP.) Luego puedes comunicarte colocando las direcciones Ethernet de origen y destino apropiadas en tus paquetes.
Un nodo normal nunca transmitirá un paquete con dirección de destino 127.0.0.1, debido al RFC 1122. Si un nodo normal recibe un paquete con dirección de destino 127.0.0.1, lo ignorará (descartará), nuevamente debido al RFC 1122. Establecer net.ipv4.conf.all.route_localnet=1 cambia eso — permite que los paquetes 127.0.0.1 se envíen y reciban como si no fueran especiales.
Por lo tanto, si un atacante tiene una conexión local a un nodo objetivo con net.ipv4.conf.all.route_localnet=1, el atacante puede enviarle un paquete con 127.0.0.1 como dirección de destino, y ese nodo objetivo responderá apropiadamente como si 127.0.0.1 fuera una dirección totalmente normal. Las dos formas más comunes de tener una conexión local a un nodo objetivo hoy en día son estar en la misma red Ethernet (dominio de difusión) que el objetivo, o ser un contenedor ejecutándose en el objetivo.
Ten en cuenta que, cuando está configurado normalmente, Linux no permitirá que el nodo atacante transmita paquetes normales destinados a 127.0.0.1. Esto se puede solucionar reconfigurando el nodo Linux del atacante (si tiene acceso root), o falsificando paquetes usando un socket raw. Los sockets raw solo requieren la capacidad del kernel Linux CAP_NET_RAW, que se otorga por defecto a los contenedores no privilegiados. Esto significa que un contenedor no privilegiado controlado por un atacante es capaz de explotar CVE-2020-8558.
En resumen, si estás usando kube-proxy o haciendo cosas ingeniosas con net.ipv4.conf.*.route_localnet, estás expuesto. Deberías dedicar algún tiempo a la modelización de amenazas para determinar cuán riesgosa es esa exposición para ti y planificar una estrategia de mitigación adecuada.
Fundamentalmente, cada host Linux con net.ipv4.conf.all.route_localnet=1 establecido es vulnerable. Si esa vulnerabilidad es interesante para un atacante depende de varios factores:
Para evaluar CVE-2020-8558, debes imaginar atacantes con diversas capacidades y responder estas preguntas desde el punto de vista de esos atacantes. (El libro de Adam Shostack "Threat Modeling: Designing for Security" describe este proceso en gran detalle.) Dos atacantes relevantes que ciertamente debes considerar son un atacante con un nodo en tu red Ethernet y un atacante que pueda ejecutar código en un pod no privilegiado en tu host. Puede haber otros atacantes interesantes que también debas considerar, dependiendo de tu entorno y necesidades.
Para ilustrar, aquí hay un ejemplo parcialmente trabajado:
Suponiendo que tengas root en una máquina Linux en el mismo dominio de difusión que el objetivo, las siguientes configuraciones te permitirán explotar CVE-2020-8558:
ip addr add 127.0.0.2/8 dev lo
ip addr del 127.0.0.1/8 dev lo
ip route add 127.0.0.1/32 via YOUR-TARGET-HERE
sysctl net.ipv4.conf.all.route_localnet=1
Debido a que algunos servicios importantes (ejem, ejem, systemd-resolved) se ejecutan en una dirección 127.0.0.0/8, agregamos una nueva para evitar romper el host. Luego, el host debe olvidar su dirección predeterminada 127.0.0.1/8. A continuación, instruimos al kernel para que enrute el tráfico para 127.0.0.1 a través de la red hacia tu objetivo, que sabe cómo acceder a 127.0.0.1. Finalmente, establecemos el infame sysctl que de otro modo bloquearía que esta configuración funcione.
Script simple en Python para probar CVE-2020-8558 enviando paquetes raw. Esto podría ser un oneliner de scapy, pero quería agregar un poco más de las comodidades del hogar. Envía un paquete a 127.0.0.1 a través de tu objetivo y comprueba si hay una respuesta.
Script en Python para explotar CVE-2020-8558 permitiendo que aplicaciones cliente TCP o UDP ordinarias se comuniquen con una IP de localhost remota mediante paquetes falsificados. Ejecuta este script, luego usa cualquier cliente TCP o UDP normal (por ejemplo, kubectl o nc) para conectarte a tu fakedestination (198.51.100.1 por defecto).
Ten en cuenta que fakedestination debe ser una dirección IP que nunca responda a paquetes y tu ruta hacia ella debe ser a través de la misma interfaz con la que accedes a tu objetivo. En el caso habitual, tanto fakedestination como el objetivo serán accesibles a través de tu interfaz de gateway predeterminada, y esto no será un gran problema.
Debido a que este script utiliza sockets raw para enviar y recibir los paquetes "localhost", funciona bien dentro de un contenedor no privilegiado normal.
Issue de Kubernetes para esta CVE en GitHub
Documentación de sysctl IP del kernel
Adam Shostack: Threat Modeling
Un saludo a Ian Coldwater, Brad Geesaman, Duffie Cooley y Laurent Bernaille. Gracias por los pensamientos, consejos y risas, a todos. ¡Honk the planet!