Este tutorial é um dos quatro que colocam em prática conceitos do Microservices de março de 2022: Rede Kubernetes :
Quer orientação detalhada sobre como usar o NGINX para ainda mais casos de uso de rede Kubernetes? Baixe nosso e-book gratuito, Gerenciando o tráfego do Kubernetes com NGINX: Um guia prático .
Você trabalha em TI para uma loja local popular que vende uma variedade de produtos, de travesseiros a bicicletas. Eles estão prestes a lançar sua primeira loja online, mas pediram a um especialista em segurança para fazer um teste de penetração no site antes de torná-lo público. Infelizmente, o especialista em segurança encontrou um problema! A loja online é vulnerável à injeção de SQL . O especialista em segurança conseguiu explorar o site para obter informações confidenciais do seu banco de dados, incluindo nomes de usuário e senhas.
Sua equipe veio até você – o engenheiro do Kubernetes – para salvar o dia. Felizmente, você sabe que a injeção de SQL (assim como outras vulnerabilidades) pode ser mitigada usando ferramentas de gerenciamento de tráfego do Kubernetes. Você já implantou um controlador Ingress para expor o aplicativo e, em uma única configuração, é capaz de garantir que a vulnerabilidade não possa ser explorada. Agora, a loja online pode ser lançada no prazo. Bom trabalho!
Este blog acompanha o laboratório da Unidade 3 de Microsserviços de março de 2022 – Padrão de segurança de microsserviços no Kubernetes , demonstrando como usar o NGINX e o NGINX Ingress Controller para bloquear injeção de SQL.
Para executar o tutorial, você precisa de uma máquina com:
Para aproveitar ao máximo o laboratório e o tutorial, recomendamos que antes de começar você:
Este tutorial usa estas tecnologias:
As instruções para cada desafio incluem o texto completo dos arquivos YAML usados para configurar os aplicativos. Você também pode copiar o texto do nosso repositório GitHub . Um link para o GitHub é fornecido junto com o texto de cada arquivo YAML.
Este tutorial inclui quatro desafios:
Neste desafio, você implanta um cluster minikube e instala o Podinfo como um aplicativo de exemplo que tem vulnerabilidades de segurança.
Implante um cluster minikube . Após alguns segundos, uma mensagem confirma que a implantação foi bem-sucedida.
$ minikube start 🏄 Pronto! O kubectl agora está configurado para usar o cluster "minikube" e o namespace "default" por padrão
Aqui você implanta um aplicativo de comércio eletrônico simples que consiste em dois microsserviços:
Execute estas etapas:
Usando o editor de texto de sua escolha, crie um arquivo YAML chamado 1-app.yaml com o seguinte conteúdo (ou copie do GitHub ).
apiVersion: apps/v1 kind: Deployment
metadata:
name: app
spec:
selector:
matchLabels:
app: app
template:
metadata:
labels:
app: app
spec:
containers:
- name: app
image: f5devcentral/microservicesmarch:1.0.3
ports:
- containerPort: 80
env:
- name: MYSQL_USER
value: dan
- name: MYSQL_PASSWORD
value: dan
- name: MYSQL_DATABASE
value: sqlitraining
- name: DATABASE_HOSTNAME
value: db.default.svc.cluster.local
---
apiVersion: v1
kind: Service
metadata:
name: app
spec:
ports:
- port: 80
targetPort: 80
nodePort: 30001
selector:
app: app
type: NodePort
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: db
spec:
selector:
matchLabels:
app: db
template:
metadata:
labels:
app: db
spec:
containers:
- name: db
image: mariadb:10.3.32-focal
ports:
- containerPort: 3306
env:
- name: MYSQL_ROOT_PASSWORD
value: root
- name: MYSQL_USER
value: dan
- name: MYSQL_PASSWORD
value: dan
- name: MYSQL_DATABASE
value: sqlitraining
---
apiVersion: v1
kind: Service
metadata:
name: db
spec:
ports:
- port: 3306
targetPort: 3306
selector:
app: db
Implante o aplicativo e a API:
$ kubectl apply -f 1-app.yaml deployment.apps/app criado serviço/app criado deployment.apps/db criado serviço/db criado
Confirme se os pods do Podinfo foram implantados, conforme indicado pelo valor Em execução
na coluna STATUS
. Pode levar de 30 a 40 segundos para que eles sejam totalmente implantados, então aguarde até que o status de ambos os pods esteja em execução
antes de prosseguir para a próxima etapa (reemitindo o comando conforme necessário).
$ kubectl get pods NOME PRONTO STATUS REINÍCIOS IDADE app-d65d9b879-b65f2 1/1 Em execução 0 37s db-7bbcdc75c-q2kt5 1/1 Em execução 0 37s
Abra o aplicativo no seu navegador:
$ minikube service app |-----------|------|-------------|--------------| | NAMESPACE | NOME | PORTA DE DESTINO | URL | |-----------|-----|-------------|--------------| | default | app | | Nenhuma porta de nó | |-----------|-----|-------------|--------------| 😿 service default/app não tem porta de nó 🏃 Iniciando túnel para service app. |-----------|------|-------------|------------------------| | NAMESPACE | NOME | PORTA DE DESTINO | URL | |-----------|------|-------------|------------------------| | default | app | | http://127.0.0.1:55446 | |-----------|-----|--------|------------------------| 🎉 Abrindo service default/app no navegador padrão...
O aplicativo de exemplo é bastante básico. Inclui uma página inicial com uma lista de itens (por exemplo, travesseiros) e um conjunto de páginas de produtos com detalhes como descrição e preço. Os dados são armazenados no banco de dados MariaDB. Cada vez que uma página é solicitada, uma consulta SQL é emitida no banco de dados.
Ao abrir a página do produto travesseiros , você pode notar que o URL termina em /product/1 . O1 é o ID do produto. Para evitar a inserção direta de código malicioso na consulta SQL, é uma prática recomendada higienizar a entrada do usuário antes de encaminhar solicitações para serviços de backend. Mas e se o aplicativo não estiver configurado corretamente e a entrada não for escapada antes de ser inserida na consulta SQL no banco de dados?
Para descobrir se o aplicativo está escapando corretamente a entrada, execute um experimento simples alterando o ID para um que não exista no banco de dados.
Alterar manualmente o último elemento na URL de1 para-1 . A mensagem de erro ID
de produto
inválido
"-1"
indica que o ID do produto não está sendo escapado – em vez disso, a string é inserida diretamente na consulta. Isso não é bom, a menos que você seja um hacker!
Suponha que a consulta ao banco de dados seja algo como:
SELECIONE * DE alguma_tabela ONDE id = "1"
Para explorar a vulnerabilidade causada por não escapar da entrada, substitua 1
com -1"
<consulta_maliciosa>
--
//
de modo que:
"
) depois-1
conclui a primeira consulta.--
//
descarta o restante da consulta.Então, por exemplo, se você alterar o elemento final na URL para ‑1"
ou 1
--
//
, a consulta é compilada para:
SELECIONE * DE alguma_tabela ONDE id = "-1" OU 1 -- //" -------------- ^ injetado ^
Isso seleciona todas as linhas do banco de dados, o que é útil em um hack. Para descobrir se esse é o caso, altere a terminação do URL para ‑1"
. A mensagem de erro resultante fornece informações mais úteis sobre o banco de dados:
Erro fatal: mysqli_sql_exception não capturado: Você tem um erro na sintaxe do seu SQL; verifique o manual que corresponde à versão do seu servidor MariaDB para saber a sintaxe correta a ser usada perto de '"-1""' na linha 1 em /var/www/html/product.php:23 Rastreamento de pilha: #0 /var/www/html/product.php(23): mysqli->query('SELECT * FROM p...') #1 {main} lançado em /var/www/html/product.php na linha 23
Agora, você pode começar a manipular o código injetado na tentativa de ordenar os resultados do banco de dados por ID:
-1" OU 1 ORDEM POR id DESC -- //
O resultado é a página do produto do último item no banco de dados.
Forçar o banco de dados a ordenar os resultados é interessante, mas não é especialmente útil se o seu objetivo for hackear. Tentar extrair nomes de usuários e senhas do banco de dados vale muito mais a pena.
É seguro assumir que há uma tabela de usuários no banco de dados com nomes de usuários e senhas. Mas como você estende seu acesso da tabela de produtos para a tabela de usuários?
A resposta é injetar código como este:
-1" UNION SELECT * FROM usuários -- //
onde
‑1"
força o retorno de um conjunto vazio da primeira consulta.UNION
força duas tabelas de banco de dados juntas – neste caso, produtos e usuários – o que permite que você obtenha informações (senhas) que não estão na tabela original ( produtos ).SELECT
*
FROM
users
seleciona todas as linhas na tabela de usuários .--
//
descarta tudo após a consulta maliciosa.Quando você modifica a URL para terminar no código injetado, você recebe uma nova mensagem de erro:
Erro fatal: mysqli_sql_exception não capturado: As instruções SELECT usadas têm um número diferente de colunas em /var/www/html/product.php:23 Rastreamento de pilha: #0 /var/www/html/product.php(23): mysqli->query('SELECT * FROM p...') #1 {main} lançado em /var/www/html/product.php na linha 23
Esta mensagem revela que as tabelas de produtos e usuários não têm o mesmo número de colunas, então a instrução UNION
não pode ser executada. Mas você pode descobrir o número de colunas por tentativa e erro, adicionando colunas (nomes de campos) uma de cada vez como parâmetros para a instrução SELECT
. Um bom palpite para o nome de um campo em uma tabela de usuários é password
, então tente isso:
# selecione 1 coluna -1" UNION SELECT senha FROM usuários; -- // # selecione 2 colunas -1" UNION SELECT senha,senha FROM usuários; -- // # selecione 3 colunas -1" UNION SELECT senha,senha,senha FROM usuários; -- / # selecione 4 colunas -1" UNION SELECT senha,senha,senha,senha FROM usuários; -- // # selecione 5 colunas -1" UNION SELECT senha,senha,senha,senha,senha FROM usuários; -- //
A última consulta é bem-sucedida (informando que há cinco colunas na tabela de usuários ) e você vê uma senha de usuário:
Neste ponto você não sabe o nome de usuário que corresponde a esta senha. Mas sabendo o número de colunas na tabela de usuários , você pode usar os mesmos tipos de consulta de antes para expor essas informações. Suponha que o nome do campo relevante seja username
. E isso está certo – a consulta a seguir expõe o nome de usuário e a senha da tabela de usuários . O que é ótimo – a menos que este aplicativo esteja hospedado em sua infraestrutura!
-1" UNION SELECT nome de usuário,nome de usuário,senha,senha,nome de usuário FROM usuários onde id=1 -- //
O desenvolvedor do aplicativo da loja online obviamente precisa prestar mais atenção à higienização da entrada do usuário (como o uso de consultas parametrizadas), mas como um engenheiro do Kubernetes, você também pode ajudar a evitar a injeção de SQL bloqueando o ataque de atingir o aplicativo. Dessa forma, não importa tanto que o aplicativo seja vulnerável.
Há muitas maneiras de proteger seus aplicativos. No restante deste laboratório, nos concentraremos em dois:
Neste desafio, você injeta um contêiner sidecar no pod para fazer proxy de todo o tráfego e negar qualquer solicitação que tenha UNION
na URL.
Primeiro, implante o NGINX Open Source como um sidecar e depois teste se ele filtra consultas maliciosas .
Observação: Estamos aproveitando essa técnica apenas para fins ilustrativos. Na realidade, implantar manualmente proxies como sidecars não é a melhor solução (mais sobre isso depois).
Crie um arquivo YAML chamado 2-app-sidecar.yaml com o seguinte conteúdo (ou copie do GitHub ). Aspectos importantes da configuração incluem:
SELECT
ou UNION
será negada (veja o primeiro bloco de localização
na seção ConfigMap
).apiVersion: apps/v1
kind: Deployment
metadata:
name: app
spec:
selector:
matchLabels:
app: app
template:
metadata:
labels:
app: app
spec:
containers:
- name: app
image: f5devcentral/microservicesmarch:1.0.3
ports:
- containerPort: 80
env:
- name: MYSQL_USER
value: dan
- name: MYSQL_PASSWORD
value: dan
- name: MYSQL_DATABASE
value: sqlitraining
- name: DATABASE_HOSTNAME
value: db.default.svc.cluster.local
- name: proxy # <-- sidecar
image: "nginx"
ports:
- containerPort: 8080
volumeMounts:
- mountPath: /etc/nginx
name: nginx-config
volumes:
- name: nginx-config
configMap:
name: sidecar
---
apiVersion: v1
kind: Service
metadata:
name: app
spec:
ports:
- port: 80
targetPort: 8080 # <-- o tráfego é direcionado para o proxy
nodePort: 30001
selector:
app: app
type: NodePort
---
apiVersion: v1
kind: ConfigMap
metadata:
name: sidecar
data:
nginx.conf: |-
events {}
http {
server {
listen 8080 default_server;
listen [::]:8080 default_server;
location ~* "(\'|\")(.*)(drop|insert|md5|select|union)" {
deny all;
}
location / {
proxy_pass http://localhost:80/;
}
}
}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: db
spec:
selector:
matchLabels:
app: db
template:
metadata:
labels:
app: db
spec:
containers:
- name: db
image: mariadb:10.3.32-focal
ports:
- containerPort: 3306
env:
- name: MYSQL_ROOT_PASSWORD
value: root
- name: MYSQL_USER
value: dan
- name: MYSQL_PASSWORD
value: dan
- name: MYSQL_DATABASE
value: sqlitraining
---
apiVersion: v1
kind: Service
metadata:
name: db
spec:
ports:
- port: 3306
targetPort: 3306
selector:
app: db
Implante o sidecar:
$ kubectl apply -f 2-app-sidecar.yaml deployment.apps/app configurado serviço/app configurado configmap/sidecar criado deployment.apps/db inalterado serviço/db inalterado
Teste se o sidecar está filtrando o tráfego retornando ao aplicativo e tentando a injeção de SQL novamente. O NGINX bloqueia a solicitação antes que ela chegue ao aplicativo!
-1" UNION SELECT nome de usuário,nome de usuário,senha,senha,nome de usuário FROM usuários onde id=1 -- //
Proteger seu aplicativo como no Desafio 3 é interessante como uma experiência educacional, mas não o recomendamos para produção porque:
Uma solução muito melhor é usar o NGINX Ingress Controller para estender a mesma proteção a todos os seus aplicativos! Os controladores de entrada podem ser usados para centralizar todos os tipos de recursos de segurança, desde o bloqueio de solicitações, como um firewall de aplicativo da Web (WAF), até autenticação e autorização.
Neste desafio, você implanta o NGINX Ingress Controller , configura o roteamento de tráfego e verifica se o filtro bloqueia a injeção de SQL .
A maneira mais rápida de instalar o NGINX Ingress Controller é com o Helm .
Adicione o repositório NGINX ao Helm:
$ helm repo adicionar nginx-stable https://helm.nginx.com/stable
Baixe e instale o NGINX Open Source‑based NGINX Ingress Controller , que é mantido pela F5 NGINX. Observe o parâmetro enableSnippets=true
: snippets são usados para configurar o NGINX para bloquear a injeção de SQL. A linha final de saída confirma a instalação bem-sucedida.
$ helm install main nginx-stable/nginx-ingress \ --set controller.watchIngressWithoutClass=true --set controller.service.type=NodePort \ --set controller.service.httpPort.nodePort=30005 \ --set controller.enableSnippets=true NOME: main ÚLTIMA IMPLANTAÇÃO: Dia Seg DD hh:mm:ss AAAA ESPAÇO DE NOMES: padrão STATUS: implantado REVISÃO: 1 CONJUNTO DE TESTES: Nenhuma NOTAS: O NGINX Ingress Controller foi instalado.
Confirme se o pod do NGINX Ingress Controller foi implantado, conforme indicado pelo valor Em execução
na coluna STATUS
.
$ kubectl get pods NOME PRONTO STATUS ... main-nginx-ingress-779b74bb8b-mtdkr 1/1 Executando ... ... RECOMEÇA IDADE ... 0 18s
Crie um arquivo YAML chamado 3-ingress.yaml com o seguinte conteúdo (ou copie do GitHub ). Ele define o manifesto do Ingress necessário para rotear o tráfego para o aplicativo (não por meio do proxy sidecar desta vez). Observe o bloco annotations:
onde um snippet é usado para personalizar a configuração do NGINX Ingress Controller com o mesmo bloco location
da definição ConfigMap no Desafio 3: ele rejeita qualquer solicitação que inclua (entre outras sequências de caracteres) SELECT
ou UNION
.
apiVersion: v1 kind: Service
metadata:
name: app-without-sidecar
spec:
ports:
- port: 80
targetPort: 80
selector:
app: app
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: entry
annotations:
nginx.org/server-snippets: |
location ~* "(\'|\")(.*)(drop|insert|md5|select|union)" {
deny all;
}
spec:
ingressClassName: nginx
rules:
- host: "example.com"
http:
paths:
- backend:
service:
name: app-without-sidecar
port:
number: 80
path: /
pathType: Prefix
$ kubectl apply -f 3-ingress.yaml serviço/aplicativo-sem-sidecar criado ingress.networking.k8s.io/entrada criada
Inicie um contêiner BusyBox descartável para emitir uma solicitação ao pod do NGINX Ingress Controller com o nome de host correto.
$ kubectl run -ti --rm=true busybox --image=busybox $ wget --header="Host: example.com" -qO- main-nginx-ingress # ...
Tente a injeção de SQL. O403
O código de status proibido
confirma que o NGINX bloqueia o ataque!
$ wget --header="Host: example.com" -qO- 'main-nginx-ingress/product/-1"%20UNION%20SELECT%20nomedeusuário,nomedeusuário,senha,senha,nomedeusuário%20FROM%20usuários%20onde%2 0id=1%20--%20//' wget: o servidor retornou um erro: HTTP/1.1 403 Proibido
O Kubernetes não é seguro por padrão. Um controlador Ingress pode mitigar vulnerabilidades de injeção de SQL (e muitas outras). Mas tenha em mente que o tipo de funcionalidade semelhante a WAF que você acabou de implementar com o NGINX Ingress Controller não substitui um WAF real, nem é um substituto para a arquitetura segura de aplicativos. Um hacker experiente ainda pode fazer o hack UNION
funcionar com algumas pequenas alterações no código. Para mais informações sobre este tópico, consulte o Guia do Pentester para Injeção de SQL (SQLi) .
Dito isso, um controlador Ingress ainda é uma ferramenta poderosa para centralizar a maior parte da sua segurança, levando a maior eficiência e segurança, incluindo casos de uso de autenticação e autorização centralizados (mTLS, logon único) e até mesmo um WAF robusto como o F5 NGINX App Protect WAF .
A complexidade dos seus aplicativos e arquitetura pode exigir um controle mais refinado. Se sua organização exige Zero Trust e criptografia de ponta a ponta , considere uma malha de serviços como a sempre gratuita F5 NGINX Service Mesh para controlar a comunicação entre serviços no cluster Kubernetes (tráfego leste-oeste). Exploramos malhas de serviço na Unidade 4, Estratégias avançadas de implantação do Kubernetes .
Para obter detalhes sobre como obter e implantar o NGINX Open Source, visite nginx.org .
Para experimentar o NGINX Ingress Controller baseado no NGINX Plus com o NGINX App Protect, comece hoje mesmo seu teste gratuito de 30 dias ou entre em contato conosco para discutir seus casos de uso .
Para experimentar o NGINX Ingress Controller baseado no NGINX Open Source, consulte as versões do NGINX Ingress Controller em nosso repositório do GitHub ou baixe um contêiner pré-criado do DockerHub .
"Esta postagem do blog pode fazer referência a produtos que não estão mais disponíveis e/ou não têm mais suporte. Para obter as informações mais atualizadas sobre os produtos e soluções F5 NGINX disponíveis, explore nossa família de produtos NGINX . O NGINX agora faz parte do F5. Todos os links anteriores do NGINX.com redirecionarão para conteúdo semelhante do NGINX no F5.com."