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:
- Bypassing Web Filters Part 1: SNI Spoofing
- Bypassing Web Filters Part 2: Host Header Spoofing
- Bypassing Web Filters Part 3: Domain Fronting
- Bypassing Web Filters Part 4: Host Header Spoofing & Domain Fronting Detection Bypasses
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):

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:

:authority
pseudo-header contains the provided hostnameNow, 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.

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 ④:

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 ③:

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
:

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
) ③:

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 ③:

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:

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

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

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:

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.

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:

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.


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:

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
- HTTP/2: https://http2.github.io/ ↩︎
- RFC 7540, Hypertext Transfer Protocol Version 2 (HTTP/2): https://httpwg.org/specs/rfc7540.html ↩︎
- RFC 7540, Hypertext Transfer Protocol Version 2 (HTTP/2), HTTP Request Format: https://httpwg.org/specs/rfc7540.html#HttpRequest ↩︎
- Fortinet Documentation, Fortigate, Domain Fronting Protection: https://docs.fortinet.com/document/fortigate/7.6.2/administration-guide/639769 ↩︎
- RFC 9114, HTTP/3: https://httpwg.org/specs/rfc9114.html#discovery ↩︎
- RFC 6066, TLS Extension Definitions, SNI: https://datatracker.ietf.org/doc/html/rfc6066#section-3 ↩︎
- 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 ↩︎
- 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 ↩︎
- 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 ↩︎
- FortiGate / FortiOS Administration Guide: config webfilter urlfilter (see
ip-addr-block
): https://docs.fortinet.com/document/fortigate/7.6.2/cli-reference/333203621 ↩︎ - 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 ↩︎
- FortiGate / FortiOS Administration Guide: Block or allow ECH TLS connections: https://docs.fortinet.com/document/fortigate/7.6.2/administration-guide/447220 ↩︎