In the previous posts of this series, we looked at different ways to bypass web filters, such as Host header spoofing and domain fronting. As we’ve learned, these techniques can be detected by proxies employing TLS inspection, by checking whether the hostname in the SNI matches the one in the HTTP Host header. If they do not match, the connection can be blocked.

But – as you know – no system is perfect. This last post of the series discusses techniques that can sometimes be used to bypass domain fronting detection and prevention methods.

Bypassing web filters blog post series:

HTTP/2 Bypass

Unlike HTTP/1.1, HTTP/21 is not a simple ASCII based protocol anymore2. The entire request is binary-encoded, the headers always compressed and some were renamed. The Host header for example does not exist anymore and was replaced with the :authority pseudo header. Because of this, the entire request must be parsed differently.

Let’s repeat the domain fronting request using HTTP/2 to our attacker system from the example of the previous blog post:

$ curl --http2 -v -H "Host: compass-test.global.ssl.fastly.net" https://creators.spotify.com/index.html
[...]
*   common name: creators.spotify.com (matched)

[...]
* using HTTP/2
* [HTTP/2] [1] OPENED stream for https://creators.spotify.com/index.html
* [HTTP/2] [1] [:method: GET]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: compass-test.global.ssl.fastly.net]
* [HTTP/2] [1] [:path: /index.html]
* [HTTP/2] [1] [user-agent: curl/8.11.1]
* [HTTP/2] [1] [accept: */*]
> GET /index.html HTTP/2
> Host: compass-test.global.ssl.fastly.net
> User-Agent: curl/8.11.1
> Accept: */*
> 
* Request completely sent off
< HTTP/2 200 
[...]

<h1>Hello from Compass</h1>

The provided Host header was automatically replaced with the :authority pseudo HTTP/2 header, as defined in the HTTP/2 standard3 (curl only shows the Host header in addition for your convenience):

HTTP/2 standard stating that the :authority pseudo header should be used instead of the Host header

This can be confirmed in Wireshark. After decompressing the HTTP headers ② from the HTTP/2 request ①, it can be seen that the provided hostname from the Host header was put into the :authority pseudo header ③④, and that the actual Host header is not present anymore:

The :authority pseudo-header contains the provided hostname

Now, a TLS-intercepting proxy cannot simply decrypt the TLS traffic and search for the Host string anymore, as it could previously with HTTP/1.1. It needs a bit more work to parse and analyze the HTTP/2 request.

If a proxy is not aware of this request format, it cannot simply compare the hostname from the HTTP request against the one from the SNI. This could therefore be used to bypass such proxies!

Fortinet describes in their documentation that the domain fronting protection does only work for HTTP/1.1 and not HTTP/24.

The protection does not work for HTTP/2.

So, another technique on your bypass checklist!

HTTP/3 Bypass

HTTP/3 uses QUIC as its underlying transport protocol, which operates over UDP. For the handshake, TLS 1.3 or higher is used. If the client is aware that the server supports HTTP/3, a QUIC connection to port 443/UDP can be established directly5:

$ curl -v --http3 https://example.net
[...]
* Connected to example.net (23.215.0.135) port 443
* using HTTP/3
* [HTTP/3] [0] OPENED stream for https://example.net/
* [HTTP/3] [0] [:method: GET]
* [HTTP/3] [0] [:scheme: https]
* [HTTP/3] [0] [:authority: example.net]
* [HTTP/3] [0] [:path: /]
* [HTTP/3] [0] [user-agent: curl/8.11.1]
* [HTTP/3] [0] [accept: */*]
> GET / HTTP/3
> Host: example.net
> User-Agent: curl/8.11.1
> Accept: */*
>
* Request completely sent off
< HTTP/3 200
< alt-svc: h3=":443"; ma=93600,h3-29=":443"; ma=93600,h3-Q050=":443"; ma=93600,quic=":443"; ma=93600; v="46,43"
< quic-version: 0x00000001
[...]

[...]
<title>Example Domain</title>

In Wireshark, we can see that the established connection ① uses UDP ② on port 443/udp with the QUIC transport protocol ③ which then contains the TLS handshake ④:

HTTP/3 traffic

The ClientHello message also includes the h3 value in the ALPN extension to indicate HTTP/3 support:

Servers can also serve HTTP/3 on other UDP ports than 443/udp. The port number can be announced in an alternative service advertisement. The following response shows how the server informs the client in the Alt-Svc HTTP response header that HTTP/3 is available on port 443/udp:

$ curl --http1.1  https://example.com
[...]

> GET / HTTP/1.1
[...]

< HTTP/1.1 200 OK
[...]
< Alt-Svc: h3=":443"; ma=93600,h3-29=":443"; ma=93600,h3-Q050=":443"; ma=93600,quic=":443"; ma=93600; v="46,43"
[...]

So, if the proxy does not support or understand this protocol, it cannot analyze the traffic and extract the hostname from the HTTP/3 requests or the SNI from the QUIC messages.

However, proxies often lack support for HTTP/3 entirely, and outgoing connections to port 443/udp are typically blocked by default in enterprise environments.

Omitting SNI Bypass

But for now, let’s go back to our trusty old version of the HTTP protocol, namely 1.1. and look at other options of defeating common detection techniques. As we know, detection basically relies on SNI inspection. But, what happens if the SNI in the TLS handshake is missing? Is this even allowed/possible? Turns out that, yes, the SNI is indeed optional6.

Such a request can be sent by using the IP address instead of the hostname in the URL:

$ curl --insecure -v -H "Host: compass-test.global.ssl.fastly.net" https://151.101.194.133/index.html
[...]
*   common name: d.sni-645-default.ssl.fastly.net (does not match '151.101.194.133')
*   subject: CN=d.sni-645-default.ssl.fastly.net
[...]

> GET /index.html HTTP/1.1
> Host: compass-test.global.ssl.fastly.net
[...]

< HTTP/1.1 200 OK
[...]

<h1>Hello from Compass</h1>

The curl output shows that the Fastly CDN used a default certificate for the hostname d.sni-645-default.ssl.fastly.net instead. That’s why the --insecure flag was used to turn off certificate verification. Wireshark confirms that no SNI was sent  ① and a default certificate ② was received for a default Fastly hostname ③:

ClientHello without an SNI

During a red teaming project, we used Host header spoofing to bypass a file upload filter that restricted files larger than a few KB. After the engagement, the customer implemented domain fronting protection on their Fortinet FortiGate and requested a retest of our technique. The original method no longer worked. However, by omitting the SNI in the ClientHello, we were able to bypass the proxy once again and establish a C2 channel for data exfiltration.

This was discovered and reported to Fortinet in August 2024. Fortinet confirmed the issue and will provide a fix for this in the future. They are investigating possible solutions that would not impact customer traffic when then SNI is missing.

Encrypted Client Hello (ECH) Bypass

The Encrypted ClientHello (ECH) is a TLS extension designed to encrypt the ClientHello message and prevent the disclosure of the SNI hostname for privacy reasons. To use this feature, clients must use DNS over HTTPS (DoH) for name resolution, as standard DNS queries would already reveal the target hostname. Additionally, an ECH configuration containing public keys required for the encryption is also retrieved via DNS.

When ECH is used, the ClientHello is divided into two parts: the outer ClientHello and the inner ClientHello. The outer ClientHello is still not encrypted and can be read by any network observer. However, the inner ClientHello is encrypted using the ECH public keys obtained from the ECH configuration via DNS over HTTPS. This ensures that the SNI contained in the inner ClientHello cannot be read by network observers.

The website https://defo.ie/ech-check.php can be used to check this behavior. The website shows which SNI was sent in the Outer ClientHello and which in the inner ClientHello. The outer SNI is cover.defo.ie and the inner SNI is defo.ie:

Testing website with two SNIs

The client fetches the ECH configuration by requesting the HTTPS DNS resource record for the inner SNI using DoH ①. The client will get the response ② containing the ECH configuration for the requested SNI (defo.ie) ③:

Querying the ECH configuration via DoH

Let’s dissect this configuration:

ECHConfig: id=72 cover.defo.ie
[...]
HPKE Key Config
  [...]
  Public Key: 15e27c19a1[...]b25fd1f75
  [...]

It instructs our client to use cover.defo.ie as the SNI in the outer Client Hello and use the public key 15e27[…] to encrypt the contents of the inner Client Hello.

In the resulting TLS ClientHello ①, the client sends the SNI cover.defo.ie in the outer ClientHello in cleartext ② but the one in the inner ClientHello in encrypted form ③:

Encrypted Client Hello

Because of this, TLS inspection proxies can still not read the hostname from the inner SNI and verify if domain fronting is performed.

However, proxies may read the DoH request used to fetch the ECH configuration by the browser. Since this is queried for the hostname of the inner SNI, a proxy could detect that this was requested and conclude that a client probably wants to connect to this host. If and how this is done again depends on the used proxy product.

Domain Fronting Protection Measurements

As so often when it comes to security topics, it’s a cat-and-mouse game between the attackers and defenders. New techniques (as shown above) may emerge, and security software vendors implement new countermeasures in turn. Let’s look at what could be done to prevent the techniques illustrated in this post.

HTTP/2 Bypass Mitigation

To mitigate the HTTP/2 bypass, HTTP/2 support could simply be disabled. Example for FortiGuard7:

Only allow HTTP/1.1 ALPN in TLS client hello

This would strip the following announcement in the ClientHello where the client informs the server that HTTP/2 can be used:

Client announces support for HTTP/2 in ClientHello

However, it’s also possible to establish an HTTP/2 from within a HTTP/1.1 request8:

Upgrading an existing HTTP/1.1 connection to HTTP/2

Here, I’m not sure how the proxy would act. I sadly could not test this so far and is left as an exercise to you, whether this is another bypass technique ;-).

HTTP/3 Bypass Mitigation

To mitigate the HTTP/3 bypass, the firewall/proxy could be used to block outgoing HTTP/3 or QUIC connections. Example for FortiGuard9:

HTTP/3 and QUIC settings

Omitting SNI Bypass Mitigation

To mitigate the bypass by omitting the SNI, connections could be blocked when the hostname appears as an IP address. Example for FortiGuard10.

Disable requests to IP addresses

But how does FortiGuard detect if a hostname is used as an IP address if we spoof a legit hostname in the Host header? If the client has an explicit proxy configured, the proxy will first receive a CONNECT request that looks like this:

CONNECT 151.101.194.133:443 HTTP/1.1
Host: 151.101.194.133:443

The spoofed Host header would follow later in the established tunnel. FortiGuard will then see that this request is for an IP address and block the request.

However, if a transparent proxy is in place, no CONNECT request is performed to the proxy and just a TLS handshake without an SNI is intercepted. Since we could not test such a setup, it’s not clear to us if this configuration would prevent the filtering bypass by omitting the SNI. Maybe I’ll get the chance in a future pentest to test such a setup.

In addition, the SNI check should be set to “strict”11. This would also block the domain fronting / host header spoofing bypasses according to what the Fortinet engineers told me:

Additional SNI checks on FortiGate

Encrypted Client Hello (ECH) Bypass Mitigation

To mitigate the bypass by using ECH, the ECH could be blocked and the ECH configuration could be stripped from DNS responses12.

Block Encrypted Client Hello
Stripping ECH Configuration from DoH responses

Important Note

Note that these measures could have a negative impact and side effects and should always be tested before wide deployment!

SNI Spoofing vs. Host Header Spoofing vs. Domain Fronting

We have now discussed three different web filter bypass techniques in this series.

Let’s recap and compare how these techniques work, which system the connection is established to, which hostname is in the SNI and Host header and how such bypasses can be detected/prevented:

Summary

Conclusion

If you have followed all posts of this series, you may have picked up a common theme quite familiar to security researchers and engineers alike. That is “It depends…”.

And indeed, as with many other security mechanisms, there is no perfect solution when it comes to web filter bypasses, both from the attacker’s and defender’s perspective. Successfully bypassing a web filter – or detecting and preventing such attacks – depends on various aspects, such as proxy and firewall products and their capabilities, browser versions, server configuration, supported protocols, and so on and so forth.

Therefore it is essential to remember that your situation and infrastructure might different from others, and therefore must be treated as such. Define and analyze your specific requirements and make sure that the measures you implement address risks and threats relevant to you.

I want to thank Alex Joss for reviewing this blog post series and for your helpful feedback and discussions.

References

  1. HTTP/2: https://http2.github.io/ ↩︎
  2. RFC 7540, Hypertext Transfer Protocol Version 2 (HTTP/2): https://httpwg.org/specs/rfc7540.html ↩︎
  3. RFC 7540, Hypertext Transfer Protocol Version 2 (HTTP/2), HTTP Request Format: https://httpwg.org/specs/rfc7540.html#HttpRequest ↩︎
  4. Fortinet Documentation, Fortigate, Domain Fronting Protection: https://docs.fortinet.com/document/fortigate/7.6.2/administration-guide/639769 ↩︎
  5. RFC 9114, HTTP/3: https://httpwg.org/specs/rfc9114.html#discovery ↩︎
  6. RFC 6066, TLS Extension Definitions, SNI: https://datatracker.ietf.org/doc/html/rfc6066#section-3 ↩︎
  7. FortiGate / FortiOS Administration Guide: HTTP/2 support in proxy mode SSL inspection: https://docs.fortinet.com/document/fortigate/7.6.2/administration-guide/710924/http-2-support-in-proxy-mode-ssl-inspection ↩︎
  8. RFC 7540, Hypertext Transfer Protocol Version 2 (HTTP/2), Upgrade from HTTP/1.1 to HTTP/2: https://httpwg.org/specs/rfc7540.html#discover-http ↩︎
  9. FortiGate / FortiOS Administration Guide: QUIC / HTTP/3 settings: https://docs.fortinet.com/document/fortigate/7.6.2/administration-guide/008405/dns-over-quic-and-dns-over-http3-for-transparent-and-local-in-dns-modes ↩︎
  10. FortiGate / FortiOS Administration Guide: config webfilter urlfilter (see ip-addr-block): https://docs.fortinet.com/document/fortigate/7.6.2/cli-reference/333203621 ↩︎
  11. FortiGate Administration Guide, Configuring an SSL/SSH inspection profile: https://docs.fortinet.com/document/fortigate/7.6.0/administration-guide/709167/configuring-an-ssl-ssh-inspection-profile ↩︎
  12. FortiGate / FortiOS Administration Guide: Block or allow ECH TLS connections: https://docs.fortinet.com/document/fortigate/7.6.2/administration-guide/447220 ↩︎