Este tutorial es uno de los cuatro que ponen en práctica los conceptos de Microservicios de marzo de 2022: Redes de Kubernetes :
¿Quieres orientación detallada sobre el uso de NGINX para aún más casos de uso de redes Kubernetes? Descargue nuestro libro electrónico gratuito, Gestión del tráfico de Kubernetes con NGINX: Una guía práctica .
Trabaja en el departamento de TI de una popular tienda local que vende una variedad de productos, desde almohadas hasta bicicletas. Están a punto de lanzar su primera tienda en línea, pero le pidieron a un experto en seguridad que realizara una prueba de penetración en el sitio antes de hacerlo público. ¡Desafortunadamente, el experto en seguridad encontró un problema! La tienda en línea es vulnerable a la inyección SQL . El experto en seguridad pudo explotar el sitio para obtener información confidencial de su base de datos, incluidos nombres de usuario y contraseñas.
Su equipo ha recurrido a usted –el ingeniero de Kubernetes– para salvar el día. Afortunadamente, sabes que la inyección de SQL (así como otras vulnerabilidades) se pueden mitigar utilizando herramientas de gestión de tráfico de Kubernetes. Ya implementó un controlador Ingress para exponer la aplicación y, en una única configuración, puede garantizar que la vulnerabilidad no pueda explotarse. Ahora la tienda en línea podrá lanzarse a tiempo. ¡Bien hecho!
Este blog acompaña al laboratorio de la Unidad 3 de Microservicios de marzo de 2022: Patrón de seguridad de microservicios en Kubernetes , que demuestra cómo usar NGINX y el controlador de ingreso NGINX para bloquear la inyección de SQL.
Para ejecutar el tutorial, necesitas una máquina con:
Para aprovechar al máximo el laboratorio y el tutorial, le recomendamos que antes de comenzar:
Este tutorial utiliza estas tecnologías:
Las instrucciones para cada desafío incluyen el texto completo de los archivos YAML utilizados para configurar las aplicaciones. También puedes copiar el texto de nuestro repositorio de GitHub . Se proporciona un enlace a GitHub junto con el texto de cada archivo YAML.
Este tutorial incluye cuatro desafíos:
En este desafío, implementa un clúster de minikube e instala Podinfo como una aplicación de muestra que tiene vulnerabilidades de seguridad.
Implementar un clúster de minikube . Después de unos segundos, un mensaje confirma que la implementación fue exitosa.
$ minikube start 🏄 ¡Listo! kubectl ahora está configurado para usar el clúster "minikube" y el espacio de nombres "default" de forma predeterminada.
Aquí se implementa una aplicación de comercio electrónico simple que consta de dos microservicios:
Realice estos pasos:
Usando el editor de texto de su elección, cree un archivo YAML llamado 1-app.yaml con el siguiente contenido (o cópielo desde GitHub ).
apiVersion: apps/v1 tipo: Implementación
Metadatos:
Nombre: app
Especificación:
Selector:
Etiquetas de coincidencia:
app: app
Plantilla:
Metadatos:
Etiquetas:
app: app
Especificación:
Contenedores:
- Nombre: app
Imagen: f5devcentral/microservicesmarch:1.0.3
Puertos:
- PuertoContenedor: 80
env:
- nombre: MYSQL_USER
valor: dan
- nombre: MYSQL_PASSWORD
valor: dan
- nombre: MYSQL_DATABASE
valor: sqlitraining
- nombre: NOMBRE_DEL_HOST_DE_BASE_DE_DATOS
valor: db.default.svc.cluster.local
---
Versión_de_API: v1
tipo: Servicio
Metadatos:
Nombre: app
Especificaciones:
Puertos:
- Puerto: 80
Puerto de destino: 80
nodePort: 30001
Selector:
Aplicación: aplicación
Tipo: NodePort
---
apiVersion: apps/v1
kind: Implementación
Metadatos:
Nombre: BD
Especificación:
Selector:
Etiquetas de coincidencia:
Aplicación: BD
Plantilla:
Metadatos:
Etiquetas:
Aplicación: BD
Especificación:
Contenedores:
- Nombre: BD
Imagen: MariaDB:10.3.32-Focal
Puertos:
- PuertoContenedor: 3306
env:
- nombre: MYSQL_ROOT_PASSWORD
valor: raíz
- nombre: MYSQL_USER
valor: dan
- nombre: MYSQL_PASSWORD
valor: dan
- nombre: MYSQL_DATABASE
valor: sqlitraining
---
apiVersion: v1
tipo: Servicio
Metadatos:
Nombre: BD
Especificación:
Puertos:
- Puerto: 3306
Puerto de destino: 3306
selector:
aplicación: db
Implementar la aplicación y la API:
$ kubectl apply -f 1-app.yaml implementación.apps/app creada servicio/app creada implementación.apps/db creada servicio/db creada
Confirme que los pods Podinfo se implementaron, como lo indica el valor En ejecución
en la columna ESTADO
. Puede tomar entre 30 y 40 segundos para que se implementen por completo, así que espere hasta que el estado de ambos pods sea En ejecución
antes de continuar con el siguiente paso (reemitir el comando según sea necesario).
$ kubectl get pods NOMBRE LISTO ESTADO REINICIO EDAD app-d65d9b879-b65f2 1/1 En ejecución 0 37 s db-7bbcdc75c-q2kt5 1/1 En ejecución 0 37 s
Abra la aplicación en su navegador:
$ minikube service app |-----------|------|-------------|--------------| | ESPACIO DE NOMBRES | NOMBRE | PUERTO DE DESTINO | URL | |-----------|------|-------------|--------------| | predeterminado | aplicación | | Sin puerto de nodo | |-----------|------|-------------|--------------| 😿 El servicio predeterminado/aplicación no tiene puerto de nodo 🏃 Iniciando túnel para la aplicación de servicio. |-----------|------|-------------|------------------------| | ESPACIO DE NOMBRES | NOMBRE | PUERTO DE DESTINO | URL | |-----------|------|-------------|------------------------| | predeterminado | aplicación | | http://127.0.0.1:55446 | |-----------|------|-------------|------------------------| 🎉 Abriendo el servicio predeterminado/aplicación en el navegador predeterminado...
La aplicação de muestra es bastante básica. Incluye una página de inicio con una lista de artículos (por ejemplo, almohadas) y un conjunto de páginas de productos con detalles como una descripción y el precio. Los datos se almacenan en la base de datos MariaDB. Cada vez que se solicita una página, se emite una consulta SQL contra la base de datos.
Cuando abres la página del producto de almohadas , es posible que notes que la URL termina en /product/1 . El1 Es el ID del producto. Para evitar la inserción directa de código malicioso en la consulta SQL, se recomienda depurar la entrada del usuario antes de reenviar las solicitudes a los servicios de backend. ¿Pero qué pasa si la aplicación no está configurada correctamente y la entrada no se escapa antes de insertarla en la consulta SQL contra la base de datos?
Para saber si la aplicación está escapando la entrada correctamente, ejecute un experimento simple cambiando el ID por uno que no exista en la base de datos.
Cambiar manualmente el último elemento de la URL desde1 a-1 . El mensaje de error Id.
de producto
no válido
"-1"
indica que no se está escapando el ID del producto; en su lugar, la cadena se inserta directamente en la consulta. ¡Eso no es bueno a menos que seas un hacker!
Supongamos que la consulta de la base de datos es algo como:
SELECCIONAR * DE alguna_tabla DONDE id = "1"
Para explotar la vulnerabilidad causada por no escapar la entrada, reemplace 1
con -1"
<consulta_maliciosa>
--
//
tal que:
"
) después de-1
Completa la primera consulta.--
//
descarta el resto de la consulta.Entonces, por ejemplo, si cambia el elemento final en la URL a -1"
o 1
--
//
, la consulta se compila a:
SELECCIONAR * DE alguna_tabla DONDE id = "-1" O 1 -- //" -------------- ^ inyectado ^
Esto selecciona todas las filas de la base de datos, lo que resulta útil en un truco. Para saber si este es el caso, cambie la terminación de la URL a -1"
. El mensaje de error resultante le brinda información más útil sobre la base de datos:
Error fatal: Excepción mysqli_sql_exception no detectada: Tiene un error en su sintaxis SQL; consulte el manual que corresponde a la versión de su servidor MariaDB para conocer la sintaxis correcta a utilizar cerca de '"-1""' en la línea 1 en /var/www/html/product.php:23 Rastreo de pila: #0 /var/www/html/product.php(23): mysqli->query('SELECT * FROM p...') #1 {main} arrojado en /var/www/html/product.php en la línea 23
Ahora, puedes comenzar a manipular el código inyectado en un intento de ordenar los resultados de la base de datos por ID:
-1" O 1 ORDEN POR id DESC -- //
El resultado es la página del producto del último artículo en la base de datos.
Forzar la base de datos a ordenar los resultados es interesante, pero no especialmente útil si el objetivo es hackear. Intentar extraer nombres de usuario y contraseñas de la base de datos vale mucho más la pena.
Es seguro asumir que hay una tabla de usuarios en la base de datos con nombres de usuario y contraseñas. Pero ¿cómo ampliar el acceso desde la tabla de productos a la tabla de usuarios?
La respuesta es inyectando código como este:
-1" UNION SELECT * FROM usuarios -- //
dónde
-1"
fuerza el retorno de un conjunto vacío de la primera consulta.UNION
fuerza la unión de dos tablas de base de datos (en este caso, productos y usuarios ), lo que permite obtener información (contraseñas) que no está en la tabla original ( productos ).SELECT
*
FROM
users
selecciona todas las filas de la tabla de usuarios .--
//
descarta todo lo que esté después de la consulta maliciosa.Cuando modificas la URL para que termine en el código inyectado, aparece un nuevo mensaje de error:
Error fatal: Excepción mysqli_sql_exception no detectada: Las instrucciones SELECT utilizadas tienen un número diferente de columnas en /var/www/html/product.php:23 Rastreo de pila: #0 /var/www/html/product.php(23): mysqli->query('SELECT * FROM p...') #1 {main} agregado en /var/www/html/product.php en la línea 23
Este mensaje revela que las tablas de productos y usuarios no tienen el mismo número de columnas, por lo que no se puede ejecutar la instrucción UNION
. Pero puede descubrir la cantidad de columnas mediante prueba y error, agregando columnas (nombres de campo) una a la vez como parámetros a la instrucción SELECT
. Una buena suposición para un nombre de campo en una tabla de usuarios es contraseña
, así que intente esto:
# seleccionar 1 columna -1" UNION SELECT contraseña FROM usuarios; -- // # seleccionar 2 columnas -1" UNION SELECT contraseña,contraseña FROM usuarios; -- // # seleccionar 3 columnas -1" UNION SELECT contraseña,contraseña,contraseña FROM usuarios; -- / # seleccionar 4 columnas -1" UNION SELECT contraseña,contraseña,contraseña,contraseña FROM usuarios; -- // # seleccionar 5 columnas -1" UNION SELECT contraseña,contraseña,contraseña,contraseña,contraseña FROM usuarios; -- //
La última consulta tiene éxito (le indica que hay cinco columnas en la tabla de usuarios ) y ve una contraseña de usuario:
En este momento no sabes el nombre de usuario que corresponde a esta contraseña. Pero al conocer el número de columnas de la tabla de usuarios , puede utilizar los mismos tipos de consulta que antes para exponer esa información. Supongamos que el nombre del campo relevante es nombre de usuario
. Y eso resulta ser correcto: la siguiente consulta expone tanto el nombre de usuario como la contraseña de la tabla de usuarios . ¡Lo cual es genial, a menos que esta aplicación esté alojada en tu infraestructura!
-1" UNION SELECT nombredeusuario,nombredeusuario,contraseña,contraseña,nombredeusuario FROM usuarios donde id=1 -- //
El desarrollador de la aplicación de la tienda online obviamente debe prestar más atención a la limpieza de la entrada del usuario (como el uso de consultas parametrizadas), pero como ingeniero de Kubernetes también puede ayudar a prevenir la inyección de SQL bloqueando el ataque para que no llegue a la aplicación. De esta forma, no importa tanto que la aplicación sea vulnerable.
Hay muchas formas de proteger tus aplicaciones. Durante el resto de este laboratorio, nos centraremos en dos:
En este desafío, inyecta un contenedor sidecar en el pod para representar todo el tráfico y denegar cualquier solicitud que tenga UNION
en la URL.
Primero implemente NGINX Open Source como sidecar y luego pruebe si filtra consultas maliciosas .
Nota: Utilizamos esta técnica únicamente con fines ilustrativos. En realidad, implementar manualmente servidores proxy como sidecars no es la mejor solución (hablaremos más sobre esto más adelante).
Crea un archivo YAML llamado 2-app-sidecar.yaml con el siguiente contenido (o cópialo desde GitHub ). Los aspectos importantes de la configuración incluyen:
SELECT
o UNION
(consulte el primer bloque de ubicación
en la sección ConfigMap
).apiVersion: apps/v1
tipo: Implementación
Metadatos:
Nombre: app
Especificación:
Selector:
Etiquetas de coincidencia:
app: app
Plantilla:
Metadatos:
Etiquetas:
app: app
Especificación:
Contenedores:
- Nombre: app
Imagen: f5devcentral/microservicesmarch:1.0.3
Puertos:
- PuertoContenedor: 80
env:
- nombre: MYSQL_USER
valor: dan
- nombre: MYSQL_PASSWORD
valor: dan
- nombre: MYSQL_DATABASE
valor: sqlitraining
- nombre: NOMBRE_DEL_HOST_DE_BASE_DE_DATOS
valor: db.default.svc.cluster.local
- nombre: proxy # <-- sidecar
imagen: "nginx"
puertos:
- puerto_del_contenedor: 8080
volumenMontajes:
- Ruta de montaje: /etc/nginx
nombre: nginx-config
volumenes:
- nombre: nginx-config
mapa de configuración:
nombre: sidecar
---
apiVersion: v1
tipo: Servicio
Metadatos:
Nombre: app
Especificaciones:
Puertos:
- Puerto: 80
Puerto de destino: 8080 # <-- el tráfico se enruta al proxy
nodePort: 30001
Selector:
Aplicación: aplicación
Tipo: NodePort
---
APIVersion: v1
Tipo: Mapa de configuración
Metadatos:
Nombre: Sidecar
Datos:
Nginx.conf: |-
Eventos {}
http {
Servidor {
Listen 8080 Servidor_predeterminado;
Listen [::]:8080 Servidor_predeterminado;
Ubicación ~* "(\'|\")(.*)(drop|insert|md5|select|union)" {
Denegar todo;
}
Ubicación / {
Proxy_pass http://localhost:80/;
}
}
}
}
---
Versión de API: apps/v1
Tipo: Implementación
Metadatos:
Nombre: BD
Especificación:
Selector:
Etiquetas de coincidencia:
Aplicación: BD
Plantilla:
Metadatos:
Etiquetas:
Aplicación: BD
Especificación:
Contenedores:
- Nombre: BD
Imagen: MariaDB:10.3.32-Focal
Puertos:
- PuertoContenedor: 3306
env:
- nombre: MYSQL_ROOT_PASSWORD
valor: raíz
- nombre: MYSQL_USER
valor: dan
- nombre: MYSQL_PASSWORD
valor: dan
- nombre: MYSQL_DATABASE
valor: sqlitraining
---
apiVersion: v1
tipo: Servicio
Metadatos:
Nombre: BD
Especificación:
Puertos:
- Puerto: 3306
Puerto de destino: 3306
selector:
aplicación: db
Implementar el sidecar:
$ kubectl apply -f 2-app-sidecar.yaml despliegue.apps/app configurado servicio/app configurado configmap/sidecar creado despliegue.apps/db sin cambios servicio/db sin cambios
Pruebe si el sidecar está filtrando el tráfico volviendo a la aplicación e intentando la inyección SQL nuevamente. ¡NGINX bloquea la solicitud antes de que llegue a la aplicación!
-1" UNION SELECT nombredeusuario,nombredeusuario,contraseña,contraseña,nombredeusuario FROM usuarios donde id=1 -- //
Proteger tu aplicación como en el Desafío 3 es interesante como experiencia educativa, pero no lo recomendamos para producción porque:
¡Una solución mucho mejor es usar NGINX Ingress Controller para extender la misma protección a todas tus aplicaciones! Los controladores de ingreso se pueden utilizar para centralizar todo tipo de funciones de seguridad, desde el bloqueo de solicitudes como lo hace un firewall de aplicação web (WAF) hasta la autenticación y la autorización.
En este desafío, implementa NGINX Ingress Controller , configura el enrutamiento de tráfico y verifica que el filtro bloquee la inyección de SQL .
La forma más rápida de instalar NGINX Ingress Controller es con Helm .
Agregue el repositorio NGINX a Helm:
$ helm repo agregar nginx-stable https://helm.nginx.com/stable
Descargue e instale el controlador de entrada NGINX de código abierto, mantenido por F5 NGINX. Tenga en cuenta el parámetro enableSnippets=true
: los fragmentos de código se utilizan para configurar NGINX y bloquear la inyección SQL. La última línea de salida confirma la instalación exitosa.
NOMBRE: main ÚLTIMA IMPLEMENTACIÓN: Día Lun DD hh:mm:ss AAAA ESPACIO DE NOMBRES: predeterminado ESTADO: implementado REVISIÓN: 1 CONJUNTO DE PRUEBAS: Ninguna NOTAS: Se ha instalado el controlador de ingreso NGINX.
Confirme que el pod del controlador de ingreso NGINX se haya implementado, como lo indica el valor En ejecución
en la columna ESTADO
.
$ kubectl get pods NOMBRE LISTO ESTADO ... main-nginx-ingress-779b74bb8b-mtdkr 1/1 En ejecución ... ... REINICIO EDAD... 0 18s
Cree un archivo YAML llamado 3-ingress.yaml con el siguiente contenido (o cópielo desde GitHub ). Define el manifiesto de ingreso necesario para enrutar el tráfico a la aplicación (no a través del proxy sidecar esta vez). Tenga en cuenta las anotaciones:
bloque donde se utiliza un fragmento para personalizar la configuración del controlador de ingreso NGINX con el mismo bloque de ubicación
que en la definición de ConfigMap en el desafío 3: rechaza cualquier solicitud que incluya (entre otras cadenas de caracteres) SELECT
o UNION
.
apiVersion: v1 tipo: Servicio
Metadatos:
Nombre: app-without-sidecar
Especificación:
Puertos:
- Puerto: 80
Puerto de destino: 80
selector:
aplicación: aplicación
---
apiVersion: networking.k8s.io/v1
tipo: Entrada
Metadatos:
Nombre: entrada
Anotaciones:
nginx.org/server-snippets: |
Ubicación ~* "(\'|\")(.*)(drop|insert|md5|select|union)" {
Denegar todo;
}
Especificación:
IngressClassName: nginx
Reglas:
- host: "example.com"
http:
Rutas:
- backend:
Servicio:
Nombre: app-without-sidecar
Puerto:
Número: 80
ruta: /
tipoDeRuta: Prefijo
$ kubectl apply -f 3-ingress.yaml servicio/aplicación-sin-sidecar creado ingress.networking.k8s.io/entry creado
Inicie un contenedor BusyBox desechable para emitir una solicitud al pod del controlador de ingreso NGINX con el nombre de host correcto.
$ kubectl run -ti --rm=true busybox --image=busybox $ wget --header="Host: ejemplo.com" -qO- main-nginx-ingress # ...
Intente la inyección SQL. El403
¡El código de estado prohibido
confirma que NGINX bloquea el ataque!
$ wget --header="Host: ejemplo.com" -qO- 'main-nginx-ingress/producto/-1"%20UNION%20SELECT%20nombreusuario,nombreusuario,contraseña,contraseña,nombreusuario%20FROM%20usuarios%20donde%2 0id=1%20--%20//' wget: el servidor devolvió un error: HTTP/1.1 403 Prohibido
Kubernetes no es seguro de forma predeterminada. Un controlador de Ingress puede mitigar las vulnerabilidades de inyección de SQL (y muchas otras). Pero tenga en cuenta que el tipo de funcionalidad similar a WAF que acaba de implementar con NGINX Ingress Controller no reemplaza un WAF real ni es un reemplazo para la arquitectura segura de aplicaciones. Un hacker inteligente aún puede hacer que el hack de UNION
funcione con algunos pequeños cambios en el código. Para obtener más información sobre este tema, consulte A Pentester's Guide to SQL Injection (SQLi) .
Dicho esto, un controlador de Ingress sigue siendo una herramienta poderosa para centralizar la mayor parte de su seguridad, lo que genera una mayor eficiencia y seguridad, incluidos casos de uso de autenticación y autorización centralizados (mTLS, inicio de sesión único) e incluso un WAF robusto como F5 NGINX App Protect WAF .
La complejidad de sus aplicaciones y arquitectura podrían requerir un control más detallado. Si su organización requiere Zero Trust y cifrado de extremo a extremo , considere una malla de servicios como F5 NGINX Service Mesh siempre gratuita para controlar la comunicación entre los servicios en el clúster de Kubernetes (tráfico de este a oeste). Exploramos las mallas de servicios en la Unidad 4, Estrategias avanzadas de implementación de Kubernetes .
Para obtener detalles sobre cómo obtener e implementar NGINX Open Source, visita nginx.org .
Para probar el controlador de ingreso NGINX basado en NGINX Plus con NGINX App Protect, comience hoy su prueba gratuita de 30 días o contáctenos para analizar sus casos de uso .
Para probar el controlador de ingreso NGINX basado en NGINX de código abierto, consulte las versiones del controlador de ingreso NGINX en nuestro repositorio de GitHub o descargue un contenedor prediseñado de DockerHub .
"Esta publicación de blog puede hacer referencia a productos que ya no están disponibles o que ya no reciben soporte. Para obtener la información más actualizada sobre los productos y soluciones F5 NGINX disponibles, explore nuestra familia de productos NGINX . NGINX ahora es parte de F5. Todos los enlaces anteriores de NGINX.com redirigirán a contenido similar de NGINX en F5.com.