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 tipo: Implantação
metadados:
nome: app
especificação:
seletor:
matchLabels:
aplicativo: app
modelo:
metadados:
rótulos:
aplicativo: app
especificação:
contêineres:
-nome: app
imagem: f5devcentral/microservicesmarch:1.0.3
portas:
-containerPort: 80
ambiente:
- nome: MYSQL_USER
valor: dan
- nome: MYSQL_PASSWORD
valor: dan
- nome: MYSQL_DATABASE
valor: sqlitraining
- nome: DATABASE_HOSTNAME
valor: db.default.svc.cluster.local
---
apiVersion: v1
tipo: Serviço
metadados:
nome: aplicativo
especificação:
portas:
- porta: 80
targetPort: 80
nóPorta: 30001
seletor:
aplicativo: aplicativo
tipo: NodePort
---
apiVersion: apps/v1
tipo: Implantação
metadados:
nome: db
especificação:
seletor:
matchLabels:
aplicativo: db
modelo:
metadados:
rótulos:
aplicativo: db
especificação:
contêineres:
-nome: db
imagem: mariadb:10.3.32-focal
portas:
-containerPort: 3306
ambiente:
- nome: MYSQL_ROOT_PASSWORD
valor: root
- nome: MYSQL_USER
valor: dan
- nome: MYSQL_PASSWORD
valor: dan
- nome: MYSQL_DATABASE
valor: sqlitraining
---
apiVersion: v1
tipo: Serviço
metadados:
nome: db
especificação:
portas:
- porta: 3306
targetPort: 3306
seletor:
aplicativo: 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
).apiVersão: apps/v1
tipo: Implantação
metadados:
nome: app
especificação:
seletor:
matchLabels:
app: app
modelo:
metadados:
rótulos:
app: app
especificação:
contêineres:
-nome: app
imagem: f5devcentral/microservicesmarch:1.0.3
portas:
-containerPort: 80
ambiente:
- nome: MYSQL_USER
valor: dan
- nome: MYSQL_PASSWORD
valor: dan
- nome: MYSQL_DATABASE
valor: sqlitraining
- nome: DATABASE_HOSTNAME
valor: db.default.svc.cluster.local
- nome: proxy # <-- sidecar
imagem: "nginx"
portas:
- containerPort: 8080
volumeMounts:
- mountPath: /etc/nginx
nome: nginx-config
volumes:
-nome: nginx-config
configMap:
nome: sidecar
---
apiVersion: v1
tipo: Serviço
metadados:
nome: aplicativo
especificação:
portas:
- porta: 80
targetPort: 8080 # <-- o tráfego é roteado para o proxy
nodePort: 30001
seletor:
aplicativo: aplicativo
tipo: NodePort
---
apiVersion: v1
tipo: ConfigMap
metadados:
nome: sidecar
dados:
nginx.conf: |-
eventos {}
http {
servidor {
escutar 8080 default_server;
escutar [::]:8080 default_server;
localização ~* "(\'|\")(.*)(drop|insert|md5|select|union)" {
negar tudo;
}
localização / {
proxy_pass http://localhost:80/;
}
}
}
---
apiVersion: apps/v1
tipo: Implantação
metadados:
nome: db
especificação:
seletor:
matchLabels:
aplicativo: db
modelo:
metadados:
rótulos:
aplicativo: db
especificação:
contêineres:
-nome: db
imagem: mariadb:10.3.32-focal
portas:
-containerPort: 3306
ambiente:
- nome: MYSQL_ROOT_PASSWORD
valor: root
- nome: MYSQL_USER
valor: dan
- nome: MYSQL_PASSWORD
valor: dan
- nome: MYSQL_DATABASE
valor: sqlitraining
---
apiVersion: v1
tipo: Serviço
metadados:
nome: db
especificação:
portas:
- porta: 3306
targetPort: 3306
seletor:
aplicativo: 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 tipo: Serviço
metadados:
nome: app-without-sidecar
especificação:
portas:
- porta: 80
targetPort: 80
seletor:
aplicativo: aplicativo
---
apiVersion: networking.k8s.io/v1
tipo: Ingress
metadados:
nome: entrada
anotações:
nginx.org/server-snippets: |
localização ~* "(\'|\")(.*)(drop|insert|md5|select|union)" {
negar tudo;
}
especificação:
ingressClassName: nginx
regras:
-host: "example.com"
http:
caminhos:
-backend:
serviço:
nome: app-without-sidecar
porta:
número: 80
caminho: /
caminhoType: 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."