CVE-2024-11234: Configuring a proxy in a PHP stream context might allow for CRLF injection in URIs 🐘
This blog post expands on a vulnerability I found in PHP, assigned CVE-2024-11234, that in some proxy configurations might lead to HTTP request smuggling attacks when unsanitized user-controlled data is used in stream functions.
Vulnerable PHP versions are:
- < 8.1.31
- < 8.2.26
- < 8.3.14
Overview
PHP offers a wide range of functions to interact with local or remote resources. These resources can be accessed using streams, which can be created using functions like fopen
, file
, file_get_contents
. When creating a stream, we can specify a custom stream context containing a set of parameters to use during the connection.
For example, in the case of HTTP streams, one could specify some special headers to use in the request, or a proxy to use to connect to the remote server.
Stream contexts are created using the stream_context_create
function, which accepts an array of pretty well-documented options. Different stream types have different available options.
In particular, both HTTP and FTP streams allow for the proxy
option, which specifies the HTTP proxy to use to connect to the remote server.
The HTTP stream also has a request_fulluri
option, which is required by some proxies to correctly forward the request. While a normal request to http://example.com/path/to/file?query
would results in the following HTTP request:
when setting request_fulluri
to true
we obtain:
The vulnerability I found allows an attacker to perform CRLF injection in the request URI when such request_fulluri
parameter is enabled, leading to HTTP request smuggling attacks.
The vuln
As I introduced before, in PHP we can create stream contexts to define parameters when accessing resources. What follows is an example of how to create a stream context to access a remote HTTP resource using a proxy:
Usually, the following piece of code would be safe:
Even if a malicious user were to provide a path containing control characters, such as the CRLF characters used to define HTTP request lines, the file_get_contents
function would sanitize the input, preventing any kind of attack. However, when using a stream context with request_fulluri
set to true, this is no longer true.
Usually, when creating a request, PHP parses the URL to extract the relevant parts, such as the host, path, and query, which all get used to build the HTTP request. During the parsing process, control characters are sanitized to prevent this kind of attack:
When setting request_fulluri
to true, though, the request is constructed using the raw URI provided as input, and not the parsed one:
This means that, when the request_fulluri
context parameter is true, if the resource URI is under the partial control of an attacker, a malicious actor could inject CRLF characters to perform a HTTP request smuggling attack.
Furthermore, if a non-HTTP stream sets a proxy
parameter in the context (right now only the FTP context supports this), the request_fulluri
parameter gets automatically silently enabled without the developer ever knowing:
Notice that, in the case of HTTP streams, setting a value for the proxy
parameter is not strictly necessary, as the real issue is in request_fulluri
.
PoC
We can test the raw query performed by PHP by simply using netcat in listening mode (nc -lvnp 1337
).
When no proxy is configured, we can see that the malicious input is correctly sanitized:
But when we use a proxy and set request_fulluri
to true
, we get request smuggling:
This also happens with HTTP proxies on FTP, without the request_fulluri
parameter:
We can verify servers would accept these requests by using a simple Nginx example configuration:
and the following test script:
resulting in the following output:
Notice how we also needed to inject the Connection: keep-alive
header in order to prevent Nginx from closing our connection.
Impact
CLRF injection in the URI leads to Server Side Request Forgery attacks (SSRF), which allows an attacker to bypass security controls, access internal endpoints, and, since we control the Host:
header, potentially access different hosts or machines.
Crafted requests can use any HTTP method and set any header to any value, including sensitive headers like Authorization
, Cookie
, Origin
, or Referer
.
Moreover, as stream functions read everything up until EOF, and don’t follow the Content-Length
header, the attacker might be able to read the HTTP response of each of the smuggled request, as we saw in the Nginx example above.
The patch
The vulnerability has been patched by simply checking whether the input URI contains either the \r
or \n
characters, and if so, returning an error:
Conclusions
This vulnerability requires a very specific scenario, and is therefore pretty hard to exploit in the wild. However, it is still a very interesting attack vector, and it was a nice introduction to PHP internals and how they can be read to better understand their inner workings.
I would like to greatly thank Marco Squarcina for helping me review my original report.
Bonus: finding this vulnerability
I found this vulnerability while playing a CTF, more specifically one of the rounds from this year’s (2024) OpenECSC competition. I was playing around with the file_get_contents
function, and in order to better understand how it generated the HTTP request, I created a stream context to proxy it through netcat.
That’s why, desperately fuzzing the input data of the function, I realized that I could inject CRLF characters in the URI, and that the request was not being sanitized as I expected. This seemed very strange to me, as this wasn’t how I remembered the function to behave. It also created a lot of confusion and frustration, as when I tried to reproduce the issue in a clean environment, I couldn’t get it to work. It was only after a lot of trial and error that I realized the issue was in the stream context, and that the request_fulluri
parameter was the culprit.
A lot of frustration also occurred when I tried to play a bit more with this, using the Burp proxy: Burp, by default, silently overrides the Connection
header to close
, making servers close the connection and blocking the smuggling attack. This initially led me to believe no server would accept my requests, until I realized what was happening.