Skip to content

Como Fazer: Teste de Penetração ao Vivo em Container

Este guia documenta como executar um teste de penetração adversarial ao vivo em container contra uma aplicação NENE2 — começando pelo setup, passando por todas as 30 fases de ataque — e registra os resultados canônicos da sessão de teste v1.5.329 (2026-05-31, 150+ casos).

O teste adota uma mentalidade de cracker: assuma que o atacante tem acesso total ao código-fonte (white-box), leu toda a documentação pública e tentará toda classe de ataque conhecida antes de desistir.


Pré-requisitos

  • Docker Compose disponível (docker compose version)
  • curl, nc (netcat), openssl, python3 instalados no host
  • Um container NENE2 em execução com credenciais de teste

1. Setup do Container

Inicie um alvo de teste isolado. Use uma porta dedicada (nunca a porta de produção) e injete credenciais de teste:

bash
# PHP built-in server target — fastest to spin up, tests raw NENE2 behaviour
NENE2_MACHINE_API_KEY=pentest-key docker compose run -d --rm \
  -e NENE2_LOCAL_JWT_SECRET=pentest-jwt-secret-32chars-min!! \
  -e APP_ENV=local \
  -e APP_DEBUG=false \
  -p 8299:80 \
  app php -S 0.0.0.0:80 -t public_html/

# Apache target — tests full stack including Apache config hardening
NENE2_MACHINE_API_KEY=pentest-key docker compose up -d app
# Available on :8200 (see port registry in CLAUDE.md §8)

Verificação de smoke baseline:

bash
curl -si http://localhost:8299/
# Expected: 200 OK, security headers present, no Server/X-Powered-By

Enumere a superfície de ataque a partir do OpenAPI:

bash
curl -s http://localhost:8299/openapi.php | grep -E "^  /"
# → /, /health, /machine/health, /examples/protected,
#   /examples/notes, /examples/notes/{id}, /examples/tags, /examples/tags/{id}

Gere credenciais de teste dentro do container:

bash
CID=$(docker ps --filter "publish=8299" --format "{{.ID}}")
VALID_JWT=$(docker exec $CID php -r "
  require 'vendor/autoload.php';
  \$v = new Nene2\Auth\LocalBearerTokenVerifier('pentest-jwt-secret-32chars-min!!');
  echo \$v->issue(['sub'=>'tester','exp'=>time()+86400]);
")

2. Fases de Ataque

Fase 1 — Confusão de Algoritmo JWT

IDAtaqueEsperadov1.5.329
J-01alg:none (assinatura vazia)401✅ BLOCKED
J-02alg:NONE (maiúsculas)401✅ BLOCKED
J-03alg:None (caixa mista)401✅ BLOCKED
J-04alg:hs256 (minúsculas)401✅ BLOCKED
J-05alg:RS256 (confusão de chave)401✅ BLOCKED
J-06Sem campo alg401✅ BLOCKED
J-07kid: ../../etc/passwd200 (sig válida)✅ SAFE — campos de header extras ignorados
J-08jku: http://evil.com200 (sig válida)✅ SAFE — sem fetch de JWK
bash
# J-01: alg:none
H=$(echo -n '{"typ":"JWT","alg":"none"}' | base64 -w0 | tr '+/' '-_' | tr -d '=')
P=$(echo -n '{"sub":"admin","exp":9999999999}' | base64 -w0 | tr '+/' '-_' | tr -d '=')
curl -si -H "Authorization: Bearer $H.$P." http://localhost:8299/examples/protected
# → 401  detail: "Token algorithm must be HS256."

Fase 1b — Manipulação de Payload JWT

IDAtaqueEsperadov1.5.329
J-09exp: 0 (epoch 1970)401 expirado✅ BLOCKED
J-10exp: null401 deve ser numérico✅ BLOCKED
J-11exp: "never"401 deve ser numérico✅ BLOCKED
J-12exp: 9999999999.9 (float)401 deve ser numérico✅ BLOCKED
J-13Payload é um array JSON401 deve ser numérico✅ BLOCKED
J-14Espaço duplo no valor Bearer401✅ BLOCKED
J-15Sem esquema Bearer401✅ BLOCKED
J-16Token de 4 segmentos (ponto extra)401 formato inválido✅ BLOCKED
J-17Apenas header + payload (sem sig)401✅ BLOCKED

Invariante-chave: exp deve ser um inteiro presente — ausência ou tipo errado é rejeitado (corrigido em v1.5.329).

Fase 2 — SQL Injection

Todos os repositórios usam queries parametrizadas com placeholder ?. Nenhuma interpolação de string raw.

IDAtaqueEsperadov1.5.329
S-01Clássico ' OR 1=1-- no title201 (armazenado como literal)✅ SAFE
S-02UNION SELECT 1,2,3--201 (armazenado como literal)✅ SAFE
S-03Boolean blind AND 1=1--201 (armazenado como literal)✅ SAFE
S-04Time-based AND SLEEP(2)--201 em <50ms✅ SAFE — SLEEP não executado
S-05SQLi em path param /notes/1' OR '1'='1200 (cast para int → 1)✅ SAFE
S-06Null byte \0' OR '1'='1201 (literal)✅ SAFE
S-07Segunda ordem: armazenar payload, depois ler200 (releitura literal)✅ SAFE
S-08SLEEP(5) em campo do corpo201 em <50ms✅ SAFE
S-10limit=UNION SELECT... na query string422 (validação)✅ SAFE
bash
# Verify parameterized queries: SLEEP is not executed
time curl -si -X POST -H "Content-Type: application/json" \
  -d '{"title":"x'\'' AND SLEEP(3)--","body":"x"}' \
  http://localhost:8299/examples/notes
# → 201 in < 100ms  (SLEEP never ran)

Fase 3 — Path Traversal / LFI / PHP Wrappers

IDAtaqueEsperadov1.5.329
P-01../../etc/passwd404✅ BLOCKED
P-02Variantes URL-encoded %2e%2e%2f (5 formas)404✅ BLOCKED
P-03Double-encoded %252e%252e404✅ BLOCKED
P-04UTF-8 overlong %c0%ae404✅ BLOCKED
P-05php://input / php://filter / data://404✅ BLOCKED
P-06LFI via parâmetro {id}404✅ BLOCKED
P-07Null byte 1%00.html200 (cast para int → 1)✅ SAFE — registro do BD para id=1 retornado
P-08.htaccess no Apache403✅ BLOCKED
P-08b.htaccess no PHP built-in server200⚠️ EXPOSED (ver VULN-01)
P-09.git/HEAD404✅ BLOCKED
P-10Arquivos de backup (.bak, .swp, ~, etc.)404✅ BLOCKED

Fase 4 — Ataques de Protocolo HTTP

IDAtaqueEsperadov1.5.329
H-01Request smuggling CL.TEsem resposta (PHP built-in bloqueia)
H-02Smuggling TE.CL405 (método incompatível na raiz)
H-03TE.TE Transfer-Encoding ofuscadosem resposta
H-04Downgrade HTTP/1.0200 (corpo correto)
H-05Abuso de proxy via URI absoluta404
H-06Header folding HTTP500 (bug do PHP built-in)⚠️ VULN-02
H-07HTTP pipeliningrespostas intercaladas✅ SAFE
H-08100 headers customizados simultâneos200✅ SAFE
H-10Upgrade para WebSocket200 (upgrade ignorado)✅ SAFE
H-12Versão HTTP inválida (HTTP/9.9)200 (PHP built-in aceita)✅ SAFE

Fase 5 — Mass Assignment / IDOR / Lógica de Negócio

IDAtaqueEsperadov1.5.329
B-01Mass assignment (id, __proto__ no corpo)201 (campos extras ignorados)✅ SAFE
B-02IDOR: DELETE da note de outro usuário204ℹ️ Esperado (exemplos não têm propriedade)
B-04ID negativo / zero404✅ SAFE
B-05ID com integer overflow404✅ SAFE
B-06DELETE e depois re-acessar o mesmo ID404✅ SAFE
B-07Condição de corrida em DELETE concorrentetodos 404 (idempotente)✅ SAFE
B-08Corpo no limite de 1MB413✅ BLOCKED

Fase 6 — Bypass de API Key

IDAtaqueEsperadov1.5.329
A-01Sem chave401✅ BLOCKED
A-02Chave na query string (?key=, ?api_key=)401✅ BLOCKED
A-03Chave no corpo da requisição401✅ BLOCKED
A-04Variações de caixa no nome do header200 (PSR-7 normaliza)✅ SAFE
A-05Espaços em branco iniciais/finais no valor200 (PSR-7 faz trim)✅ SAFE
A-06Barra dupla //machine/health401 sem chave, 200 com✅ SAFE
A-07X-Original-URL / X-Rewrite-URL200 (header ignorado)✅ SAFE
A-08Bypass via preflight OPTIONS405✅ BLOCKED
A-09Método HEAD401✅ BLOCKED
A-10Força bruta de senhas comuns401 todos✅ BLOCKED
A-11Path URL-encoded (%6Dachine)404✅ BLOCKED
bash
# Timing attack: hash_equals used → constant-time comparison
time (for i in $(seq 1 10); do
  curl -so /dev/null -H "X-NENE2-API-Key: a" http://localhost:8299/machine/health
done)
time (for i in $(seq 1 10); do
  curl -so /dev/null -H "X-NENE2-API-Key: pentest-key" http://localhost:8299/machine/health
done)
# → timing difference < 5ms over 10 requests: SAFE

Fase 7 — Injeção / XSS / SSTI / Execução de Código

IDAtaqueEsperadov1.5.329
I-01XSS <script>alert(1)</script> armazenado201, retornado como string JSON✅ SAFE — codificação JSON neutraliza
I-02SSTI 49 / ${7*7}201, armazenado literalmente✅ SAFE — sem template engine
I-03PHP <?php system("id"); ?>201, armazenado como literal✅ SAFE
I-04Log4Shell ${jndi:ldap://...}200 (header ignorado)✅ SAFE — PHP, não Java
I-05JSON aninhado em 1000 níveis400 (limite de parse do PHP)✅ BLOCKED
I-06Caracteres de controle Unicode BiDi201 (armazenado)✅ SAFE — risco apenas de exibição
I-07Chaves JSON duplicadasúltimo valor vence (comportamento PHP)ℹ️ INFO-01

Nota sobre XSS armazenado: Payloads de XSS são armazenados e retornados verbatim nas respostas JSON. Como a API é apenas JSON (Content-Type: application/json + X-Content-Type-Options: nosniff), os navegadores não executarão o script. O risco só se materializa se outra aplicação renderizar esses dados em um contexto HTML sem escapar.

Fase 8 — Desserialização / PHP Object Injection

IDAtaqueEsperadov1.5.329
D-01Wrapper phar:// em path param404✅ BLOCKED
D-02Payload de serialização PHP O:8:"stdClass":...400 (corpo inválido)✅ BLOCKED
D-03Form URL-encoded com payload de serialização400 (Content-Type errado)✅ BLOCKED

Fase 9 — Injeção de Header / Response Splitting

IDAtaqueEsperadov1.5.329
R-01Injeção de header Location via id da note criada/examples/notes/<int>✅ SAFE — apenas int
R-02CRLF em WWW-Authenticate via erro de JWTmensagem fixa sanitizada✅ SAFE
R-03Content-Type sniffingX-Content-Type-Options: nosniff✅ SAFE
R-04ClickjackingX-Frame-Options: SAMEORIGIN✅ SAFE

Fase 10 — Bypass de CORS / SOP

IDAtaqueEsperadov1.5.329
C-01Origin: null (iframe sandboxed)Vary: Origin, sem header ACAO✅ SAFE
C-02CRLF no header Originsanitizado pela camada curl/http✅ SAFE
C-03Cache poisoning via header VaryVary: Origin presente✅ SAFE
C-04Preflight com método injetadométodo ignorado pelo PHP✅ SAFE
C-05Access-Control-Allow-Origin: *header ausente (allowlist vazia)✅ SAFE

Fases 11-20 — Codificação / Protocolo / Timing

IDAtaqueResultado
E-01Emoji / Unicode alto em JSON✅ 201 (armazenado corretamente)
E-02Override BiDi RTL (risco de spoofing)✅ 201 (apenas exibição)
E-05SQLi de paginação via query params✅ 422 (validado como inteiro)
H-06bHeader Authorization com folding⚠️ 500 (bug do PHP built-in)
20X-Request-Id de 129 caracteres rejeitado✅ Servidor gera novo ID aleatório
21Log injection via X-Request-Id %0a✅ Rejeitado (caracteres inválidos)
22Apache ServerTokens/ServerSignature✅ apenas Server: Apache
23Escalada de privilégio via JWT sub=admin✅ Claims não usadas para authz
26Replay de JWT (expirado há 2s)✅ 401 Token has expired.
27Divulgação de stack trace em 500✅ Apenas mensagem genérica
28XSS em instance do Problem Details✅ URL-encoded (seguro)
29SSRF via endpoint de health check✅ Nenhuma URL aceita
15Oráculo de timing de API keyhash_equals — diff < 5ms

3. Descobertas

VULN-01 — .htaccess legível a partir do PHP built-in server ⚠️ MEDIUM

Gatilho: curl http://localhost:8299/.htaccess
Resposta: 200 + conteúdo completo do arquivo (regras de rewrite do Apache)
Causa raiz: O servidor built-in do PHP (php -S) não aplica restrições de acesso a .htaccess — ele trata .htaccess como um arquivo estático.
Impacto: Revela as regras de URL rewrite. O conteúdo não é secreto (sem senhas/tokens), mas confirma o padrão de rewrite-para-index-php.
Mitigação: Use o container Apache (docker compose up -d app) em vez de php -S para testes sensíveis à segurança. O Apache retorna corretamente 403.

bash
# Apache (correct): 403 Forbidden
curl -si http://localhost:8200/.htaccess | head -1

# PHP built-in server (exposed): 200 OK
curl -si http://localhost:8299/.htaccess | head -1

VULN-02 — Header folding HTTP derruba o PHP built-in server ⚠️ LOW

Gatilho:

GET / HTTP/1.1\r\nHost: localhost\r\nX-NENE2-API-Key:\r\n <key>\r\n\r\n

Resposta: HTTP/1.0 500 Internal Server Error (corpo vazio)
Causa raiz: O servidor HTTP built-in do PHP não suporta header folding RFC 7230 (depreciado, mas ainda válido em HTTP/1.1). O código do framework NENE2 não está envolvido.
Impacto: Apenas em desenvolvimento (PHP built-in server). O Apache trata headers com folding corretamente.

INFO-01 — Chaves JSON duplicadas: último valor vence

{"title":"first","title":"INJECTED"}title = "INJECTED"
Comportamento padrão do json_decode do PHP. A validação se aplica ao valor final (último), então não há caminho de bypass de validação. Anotado por consciência.


4. Invariantes de Segurança Verificadas

Estas garantias se mantiveram em todos os 150+ casos de teste:

InvarianteVerificação
Todas as queries SQL parametrizadasSLEEP não executado; payloads de injeção armazenados como literais
JWT deve ser HS256 + sig válida + exp inteiroTodas as 17 variantes de ataque JWT bloqueadas
API key verificada com hash_equalsDiferença de timing < 5ms em 10 iterações
Overflow de Content-Length tratado413 com headers corretos, sem vazamento de warning do PHP
Security headers em toda respostaCSP / XCTO / XFO / Referrer-Policy / Permissions-Policy confirmados
Server: / X-Powered-By: removidosNenhum dos headers presente nas respostas do Apache
Stack traces nunca no corpo do 500Apenas o genérico "The server encountered an unexpected condition."
Path traversal bloqueadoTodas as 15 variantes de codificação retornam 404
Arquivos .env / .git / backupTodos 404 no document root
CORS padrão: sem origens permitidasAccess-Control-Allow-Origin ausente para origens arbitrárias

5. Executando a Suíte de Testes

Repetição mínima viável das verificações-chave (< 5 minutos):

bash
TARGET=http://localhost:8299
APIKEY=pentest-key
SECRET=pentest-jwt-secret-32chars-min!!
CID=$(docker ps --filter "publish=8299" --format "{{.ID}}")

# 1. JWT alg:none
H=$(echo -n '{"typ":"JWT","alg":"none"}' | base64 -w0 | tr '+/' '-_' | tr -d '=')
P=$(echo -n '{"sub":"admin","exp":9999999999}' | base64 -w0 | tr '+/' '-_' | tr -d '=')
curl -si -H "Authorization: Bearer $H.$P." $TARGET/examples/protected | grep "HTTP/"
# expected: 401

# 2. SQL injection time-based
time curl -so /dev/null -X POST -H "Content-Type: application/json" \
  -d '{"title":"x'\'' AND SLEEP(3)--","body":"x"}' $TARGET/examples/notes
# expected: < 500ms total

# 3. Path traversal
curl -si "$TARGET/%2e%2e/%2e%2e/etc/passwd" | grep "HTTP/"
# expected: 404

# 4. Content-Length overflow
curl -si -X POST -H "Content-Length: 9999999999999" $TARGET/ | head -3
# expected: 413 Request Entity Too Large (not 200 + PHP warning)

# 5. API key timing
time (for i in $(seq 1 10); do
  curl -so /dev/null -H "X-NENE2-API-Key: a" $TARGET/machine/health
done)
# expected: similar timing to correct key (hash_equals)

# 6. .htaccess exposure (Apache only)
curl -si http://localhost:8200/.htaccess | grep "HTTP/"
# expected: 403

# 7. JWT exp required
NEXP=$(docker exec $CID php -r "
  require 'vendor/autoload.php';
  \$v = new Nene2\Auth\LocalBearerTokenVerifier('$SECRET');
  echo \$v->issue(['sub'=>'user1']);
")
curl -si -H "Authorization: Bearer $NEXP" $TARGET/examples/protected | grep "detail"
# expected: "Token must contain a numeric exp claim."

Relacionados

Publicado sob a licença MIT.