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 key✅ hash_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.