Laboratórios de hacking
Como escrever um bom relatório de pentest
Estruture um relatório de pentest e escreva um achado que outra pessoa consiga reproduzir, com evidência, impacto, vetor CVSS v4.0 e correção.
Crie uma rede Docker sem acesso à internet, rode o OWASP Juice Shop nela e publique uma cópia para o navegador só em 127.0.0.1.
Um laboratório só é tão seguro quanto os caminhos que entram e saem dele. Se a sua aplicação vulnerável consegue acessar a internet, ela pode fazer chamadas para lugares que você nunca quis tocar. Se ela escuta em todas as interfaces da sua máquina, qualquer notebook na mesma rede Wi-Fi consegue alcançá-la. O Docker dá controle sobre os dois caminhos com alguns comandos. Este guia usa o OWASP Juice Shop como alvo e percorre cada um deles.
Você vai criar uma rede sem acesso para fora, subir o Juice Shop nela, chamar a aplicação a partir de um segundo contêiner e provar que essa rede não alcança a internet. Depois vai rodar uma cópia separada para o seu navegador, publicada apenas na interface de loopback, para que nada na sua rede local consiga vê-la. Isolamento é o primeiro controle de segurança de qualquer laboratório. Todos os exercícios seguintes, da varredura à prática de exploração, dependem disso.
Se você ainda não rodou o Juice Shop, comece por OWASP Juice Shop com Docker. Este artigo pressupõe os conceitos básicos de contêineres explicados nesse guia.
Um laboratório de segurança roda software feito para ser quebrado. O Juice Shop tem falhas intencionais, e é exatamente essa a graça. O risco não está na aplicação em si. O risco está no que acontece quando uma falha, uma configuração errada ou um erro de digitação dá à aplicação, ou a uma ferramenta que você roda contra ela, um caminho para outro lugar.
Três casos mostram como isso dá errado:
-p 3000:3000 puro. O Docker passa a aceitar conexões em todas as interfaces do host, então outros dispositivos da sua rede conseguem acessar a aplicação vulnerável.O isolamento cobre cada um desses casos. Uma rede interna não tem rota para fora. Uma porta presa ao loopback não tem caminho pela LAN. Uma rede dedicada separa os contêineres do laboratório dos seus outros projetos. Nenhum desses passos é difícil. Eles só são fáceis de esquecer quando você está com pressa, e por isso este guia manda verificar cada um.
Você precisa de três ideias antes de os comandos fazerem sentido.
O Docker conecta contêineres a redes virtuais. O driver padrão é o bridge. Cada contêiner em uma rede bridge recebe o próprio endereço IP em uma sub-rede privada. No Linux, a bridge roda no próprio host. No Windows e no macOS, o Docker Desktop executa o motor dentro de uma máquina virtual Linux, então a bridge fica dentro dessa VM.
Contêineres na bridge padrão conseguem se falar pelo endereço IP, mas não pelo nome. Uma rede bridge criada por você, com docker network create, passa a resolver nomes de contêineres por DNS. O Docker roda um servidor DNS embutido para essas redes. Por isso o contêiner de ferramenta deste guia consegue chamar http://juice:3000 sem depender de um IP que muda depois de um reinício.
Quando você cria uma rede com --internal, o Docker restringe o acesso externo a ela. Segundo a documentação, os contêineres de uma rede interna conversam entre si, mas não com nada fora dela. Isso inclui a internet. Você vai ver isso no passo 4, quando a consulta de nome para um host externo falha.
Por padrão, a porta de um contêiner não é publicada para outras máquinas. A opção -p ou --publish muda isso. Ela mapeia um endereço e uma porta do host para uma porta do contêiner, e o endereço que você escolhe define quais interfaces aceitam conexões.
-p 3000:3000 escuta em todas as interfaces do host, inclusive o endereço da sua LAN. -p 127.0.0.1:3000:3000 escuta só no loopback, então apenas programas da mesma máquina conseguem conectar. A documentação do Docker sobre publicação de portas mostra exatamente essa forma. A diferença é um prefixo, e ela importa.
O laboratório tem cinco partes:
juicenet, uma rede interna sem rota de saída.juice, o Juice Shop conectado somente à juicenet, sem porta publicada.curlimages/curl na juicenet que chama o juice pelo nome.example.com, o que deve falhar.juice-local, uma segunda instância do Juice Shop publicada em 127.0.0.1:3000 para o seu navegador.O contêiner juice é o que você isola. O juice-local é uma cópia de conveniência para navegar. Ele roda na rede padrão, então consegue acessar a internet. Tenha isso em mente e não coloque credenciais reais nem dados pessoais na aplicação.
Você precisa de:
docker info deve mostrar os detalhes do servidor sem erros. Na máquina usada neste guia, o Docker Engine 29.8.2 rodava no Windows 11 por meio do Docker Desktop.-w, entre aspas simples, precisam de outra forma de citação.Confira os nomes antes de escolhê-los. Outros projetos da sua máquina podem já usar nomes como juice ou juicenet. Rode isto primeiro:
docker ps -a --format '{{.Names}}'
docker network ls --format '{{.Name}}'Se alguma das listas mostrar juice, juice-local ou juicenet, escolha outros nomes ou remova antes os antigos. Não apague nada que você não criou.
Confira também a imagem que você vai rodar. O Juice Shop publica rótulos de versão na imagem:
docker image inspect bkimminich/juice-shop --format '{{index .Config.Labels "org.opencontainers.image.version"}}'Esse comando falha até a imagem estar local, porque o primeiro docker run é que a baixa. Na máquina usada neste guia, a imagem informou a versão 20.3.0. A sua pode informar outra, e vale anotar qual é.
docker network create --internal juicenetO Docker imprime o ID da nova rede. O seu será diferente. Foi isto que apareceu na máquina de teste:
44a865311b8b0df7808f81dccd11f4b50ca38800d7a38a6d17bc80fc104dadf9Confirme que a flag foi aplicada:
docker network inspect juicenet --format '{{.Internal}}'trueSe a saída for false, você criou a rede sem --internal. Remova com docker network rm juicenet e crie de novo. Não continue antes de ver true, porque todos os testes seguintes partem do pressuposto de que a flag está ativa.
docker run -d --name juice --network juicenet bkimminich/juice-shopCada opção tem uma função. -d roda o contêiner em segundo plano. --name juice dá a ele um nome DNS que outros contêineres da juicenet conseguem resolver. --network juicenet conecta o contêiner só a essa rede interna. O comando não tem -p, então o contêiner não publica porta alguma para o host.
O Docker imprime o ID do contêiner:
d9f182bbbec2dc8db7dcde02f3e94b054274b6cf8addb1a5f994e9ee23ddebf8A aplicação dentro do contêiner escuta na porta 3000. O Juice Shop precisa de um tempo para iniciar depois que o contêiner sobe, então não teste logo em seguida. Use um laço que verifica se um contêiner de ferramenta recebe HTTP 200:
for i in $(seq 1 30); do
s=$(docker run --rm --network juicenet curlimages/curl -s -o /dev/null -w '%{http_code}\n' http://juice:3000)
echo "tentativa $i: $s"
[ "$s" = "200" ] && break
sleep 3
doneEnquanto a aplicação sobe, o laço imprime 000, que significa que o curl não conseguiu conectar. Na máquina de teste saiu 000 quatro vezes e 200 na quinta tentativa. A sua pode levar mais ou menos tentativas. O laço desiste depois de 30. Se você nunca vir um 200, confira docker logs juice para ver os erros.
docker run --rm --network juicenet curlimages/curl -s -o /dev/null -w '%{http_code}\n' http://juice:3000200Leia cada parte do comando. --rm remove o contêiner de ferramenta quando ele termina. --network juicenet coloca a ferramenta na mesma rede interna da aplicação. curlimages/curl é uma imagem pequena que contém o curl. http://juice:3000 usa o nome do contêiner, que o Docker resolve dentro da juicenet. A flag -s esconde a barra de progresso, -o /dev/null descarta o corpo da resposta e -w imprime só o código de status.
Esse é o padrão que você vai reutilizar. Um scanner, um proxy ou um script entra na rede do laboratório e aponta para o contêiner pelo nome. Nada no seu host precisa rotear para a aplicação.
A tag da imagem aqui não está fixada, então o Docker baixa o que for latest no momento. Para um laboratório que você pretende repetir, fixe a tag depois de testá-la. O princípio é o mesmo.
Um teste que passa só significa algo quando um teste que falha vem depois. O passo 3 mostrou que o contêiner de ferramenta alcança a aplicação, então a rede funciona. Agora verifique o outro sentido:
docker run --rm --network juicenet curlimages/curl -sS -m 5 https://example.com; echo "exit=$?"curl: (28) Resolving timed out after 5000 milliseconds
exit=28A flag -m 5 limita a requisição a cinco segundos. -sS esconde a barra de progresso, mas mantém os erros visíveis. O código de saída 28 é o código de timeout do curl. A falha acontece na resolução de nome. A consulta de example.com nunca recebe resposta, o que combina com uma rede sem rota para fora, então o curl nem chega a abrir uma conexão.
Esse é o resultado que você quer. O laboratório alcança os próprios contêineres e não alcança o exterior. Anote as saídas dos passos 3 e 4 no seu caderno de laboratório, junto com a data e a versão do Docker. Esse registro ajuda quando você voltar ao laboratório e algo tiver mudado.
Seu navegador não resolve o nome juice, e o contêiner juice não publica porta, então ele não pode ser acessado a partir do host. Para navegar, suba uma segunda cópia e publique-a no loopback:
docker run -d --name juice-local -p 127.0.0.1:3000:3000 bkimminich/juice-shopA opção -p 127.0.0.1:3000:3000 mapeia a porta 3000 da interface de loopback para a porta 3000 dentro do contêiner. Essa cópia usa a bridge padrão, não a juicenet, então não está isolada da internet. É uma escolha consciente para uma cópia de conveniência. Se você precisa de uma configuração mais restrita, terá de desenhar um arranjo separado e testá-lo.
A cópia demora um pouco para subir. Na máquina de teste, a cópia respondeu 200 na décima tentativa. Use o mesmo tipo de laço de antes, apontado para o host:
for i in $(seq 1 30); do
s=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3000/)
echo "tentativa $i: $s"
[ "$s" = "200" ] && break
sleep 3
doneConfirme onde a porta está presa:
docker port juice-local3000/tcp -> 127.0.0.1:3000Agora confira a LAN. Descubra o endereço IPv4 da sua máquina na rede local. Use ipconfig no Windows ou ip addr no Linux. Depois, requisite a aplicação nesse endereço:
curl -s -m 5 -o /dev/null -w '%{http_code}\n' http://SEU-IP-DA-LAN:3000/Troque SEU-IP-DA-LAN pelo seu endereço. Na máquina de teste, essa requisição retornou 000, o que significa que o curl não conseguiu conectar, e ele saiu com código 7. A mesma requisição para 127.0.0.1 retornou 200. Essa diferença é o ponto central da publicação no loopback.
Agora abra http://127.0.0.1:3000 no navegador. Você deve ver a página inicial do Juice Shop. Use a aplicação apenas nessa mesma máquina.
Rode docker ps para ver os dois contêineres lado a lado:
docker ps --filter name=juice --format 'table {{.Names}} {{.Ports}}'NAMES PORTS
juice-local 127.0.0.1:3000->3000/tcp
juice 3000/tcpA linha do juice mostra 3000/tcp sem endereço do host. Isso quer dizer que a porta existe dentro da rede, mas não está mapeada para o host. A linha do juice-local mostra a publicação no host.
Pare e remova o laboratório. Os contêineres precisam sair antes da rede, porque o Docker se recusa a remover uma rede que ainda tem contêineres conectados:
docker rm -f juice juice-local && docker network rm juicenetjuice
juice-local
juicenetO Docker imprime os nomes que removeu. Depois confirme que nada sobrou:
docker ps -a --filter name=juice --format '{{.Names}}'
docker network ls --filter name=juicenet --format '{{.Name}}'Os dois comandos não devem imprimir nada. Filtros casam parte do nome, então leia a saída antes de assumir que ela está completa, e remova só o que você iniciou.
O --rm só remove os contêineres descartáveis de ferramenta. Contêineres iniciados com -d ficam até você removê-los, e é por isso que este passo existe.
Publicar sem endereço. -p 3000:3000 escuta em todas as interfaces do host. Use o prefixo 127.0.0.1: para qualquer aplicação do laboratório que você acesse da própria máquina. Confira o resultado com docker port.
Esquecer o --internal. A rede funciona sem a flag, e nada avisa. Rode docker network inspect juicenet --format '{{.Internal}}' e procure true.
Conectar a aplicação à rede padrão. Um contêiner iniciado sem --network entra na bridge padrão, que alcança o exterior. Se a aplicação deve ficar isolada, dê nome à rede toda vez.
Confiar em um teste que falhou. Um erro no passo 4 pode vir do DNS, de um proxy, de uma VPN ou de um erro de digitação. O passo 3 é o controle que mostra se o teste está medindo a coisa certa.
Usar o nome a partir do host. O nome juice só resolve para contêineres da juicenet. O seu navegador não consegue usá-lo. Por isso existe a cópia para o navegador.
Deixar contêineres rodando. Um contêiner em segundo plano continua consumindo recursos e mantém a porta presa. Termine cada sessão com o passo de limpeza e rode docker ps depois.
A opção de rede interna é um controle, e ele tem limites.
Ele não impede uma ferramenta na juicenet de alcançar todos os outros contêineres dessa rede. Contêineres da mesma rede conseguem se falar, e é isso que faz o passo 3 funcionar. Mantenha serviços não relacionados fora da rede do laboratório.
A publicação no loopback protege você de outras máquinas da sua LAN. Ela não protege contra programas que já rodam no seu próprio computador, porque esses programas também conseguem conectar em 127.0.0.1.
O teste verifica um nome e um host externo. Ele não prova que todos os caminhos de saída estão fechados. O comportamento do Docker pode variar entre Docker Desktop, motores Linux e configurações sem root. Rode as verificações de novo na máquina que você realmente usa.
Por fim, este laboratório só mostra que o isolamento funciona. Ele não mostra que o Juice Shop é seguro para ser exposto. Não é, e esse é o motivo para mantê-lo isolado.
Depois que o laboratório funcionar, algumas pequenas mudanças o tornam mais útil:
juicenet, como um banco de dados, e confirme que a aplicação o alcança pelo nome. É assim que os laboratórios com vários contêineres são montados.juicenet é um bom primeiro alvo, porque você controla todos os contêineres dela.Laboratórios de hacking
Estruture um relatório de pentest e escreva um achado que outra pessoa consiga reproduzir, com evidência, impacto, vetor CVSS v4.0 e correção.
Laboratórios de hacking
Monte um laboratório que você pode atacar: arquivo de escopo, portas do Docker no loopback, rede isolada, caderno de notas e desmontagem limpa.
Laboratórios de hacking
Rode o OWASP Juice Shop em um contêiner Docker restrito à sua máquina, confirme que ele responde e pare-o sem deixar nada rodando.