BLOG | NGINX

Tutorial do NGINX: Proteja os aplicativos do Kubernetes contra injeção de SQL

NGINX-Parte-de-F5-horiz-preto-tipo-RGB
Miniatura de Daniele Polencic
Daniele Polencic
Publicado em 22 de março de 2022

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!

Visão geral do laboratório e do tutorial

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:

  • 2 CPUs ou mais
  • 2 GB de memória livre
  • 20 GB de espaço livre em disco
  • Conexão de internet
  • Gerenciador de contêiner ou máquina virtual, como Docker, Hyperkit, Hyper-V, KVM, Parallels, Podman, VirtualBox ou VMware Fusion/Workstation
  • minikube instalado
  • Leme instalado
  • Uma configuração que permite iniciar uma janela do navegador. Se isso não for possível, você precisa descobrir como acessar os serviços relevantes por meio de um navegador.

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:

  1. Implantar um cluster e um aplicativo vulnerável
  2. Hackear o aplicativo
  3. Use um contêiner NGINX Sidecar para bloquear determinadas solicitações
  4. Configurar o NGINX Ingress Controller para filtrar solicitações

Desafio 1: Implantar um cluster e um aplicativo vulnerável

Neste desafio, você implanta um cluster minikube e instala o Podinfo como um aplicativo de exemplo que tem vulnerabilidades de segurança.

Crie um cluster Minikube

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 

Instale o aplicativo vulnerável

Aqui você implanta um aplicativo de comércio eletrônico simples que consiste em dois microsserviços:

  • Um banco de dados MariaDB
  • Um microsserviço PHP que se conecta ao banco de dados e recupera dados

Execute estas etapas:

  1. 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 
    
  2. 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 
    
  3. 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 
    
  4. 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... 
    

Desafio 2: Hackear o aplicativo

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.

  • Para a página inicial, todos os itens no banco de dados são recuperados.
  • Para uma página de produto, o item é obtido pelo ID.

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?

Exploração 1

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:

  • As aspas ( " ) depois-1 conclui a primeira consulta.
  • Você pode adicionar sua própria consulta maliciosa após as aspas.
  • A sequência -- // 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.

Exploração 2

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 .
  • A sequência -- // 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 -- //

Desafio 3: Use um contêiner NGINX Sidecar para bloquear determinadas solicitações

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:

Implementar o NGINX Open Source como um Sidecar

  1. Crie um arquivo YAML chamado 2-app-sidecar.yaml com o seguinte conteúdo (ou copie do GitHub ). Aspectos importantes da configuração incluem:

    • Um contêiner sidecar executando o NGINX Open Source é iniciado na porta 8080.
    • O NGINX encaminha todo o tráfego para o aplicativo.
    • Qualquer solicitação que inclua (entre outras sequências de caracteres) SELECT ou UNION será negada (veja o primeiro bloco de localização na seção ConfigMap ).
    • O serviço do aplicativo roteia todo o tráfego primeiro para o contêiner NGINX.
    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
    
  2. 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 o Sidecar como um filtro

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 -- //

Desafio 4: Configurar o NGINX Ingress Controller para filtrar solicitações

Proteger seu aplicativo como no Desafio 3 é interessante como uma experiência educacional, mas não o recomendamos para produção porque:

  • Não é uma solução de segurança completa.
  • Não é escalável (você não pode aplicar facilmente essa proteção a vários aplicativos).
  • Atualizá-lo é complicado e ineficiente.

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 .

Implantar o NGINX Ingress Controller 

A maneira mais rápida de instalar o NGINX Ingress Controller é com o Helm .  

  1. Adicione o repositório NGINX ao Helm: 

    $ helm repo adicionar nginx-stable https://helm.nginx.com/stable  
    
  2. 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.  
    
  3. 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
    

Direcione o tráfego para seu aplicativo

  1. 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 
    
  2. Implante o recurso Ingress: 
  3. $ kubectl apply -f 3-ingress.yaml serviço/aplicativo-sem-sidecar criado ingress.networking.k8s.io/entrada criada 
    

Verificar operação do filtro

  1. 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    # ...
    
  2. 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 
    

PRÓXIMOS PASSOS

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."