TL;DR Some hosting providers implemented http-01 having one part of the challenge key reflected in the response. This resulted in a huge amount of websites being vulnerable to XSS just because of their implementation of the http-01 ACME-challenge.

It is now almost half a year ago since Frans’ research on different Let’s Encrypt verification methods, which resulted in a blog post about how TLS SNI could be exploited to issue certifications for other domains on a shared host.

Even though that was the only report published, other verification methods were looked into as well, such as http-01. This verification method works by having Let’s Encrypt request a file located in /.well-known/acme-challenge/KEY1 and expects a response in the format of KEY1.KEY2 .

As KEY1 is in both the response and the request, some hosting providers, that used an ACME enabled certificate issuer (Let’s Encrypt is just one of them), created a solution where the first key, KEY1 , would be reflected from the URL and combined with a fixed KEY2 inside the response.

When requesting:

/.well-known/acme-challenge/ABC123

The response would look something like this:

ABC123.XYZ567

The possibility of XSS here is obvious, but there are a few mitigations to take into consideration:

Content-type is not set to HTML.

Web browsers do URL-encode the request, so if the raw request is reflected you cannot inject special characters as they would get urlencoded ( < would for example end up as %3c ).

would for example end up as ). XSS auditor in some web browsers might catch the reflected value and block the JavaScript from triggering.

We found bypasses for all three cases and the issues were reported to two major web hosting companies, as it caused all their customers to be vulnerable. One being a big international service and the other service is one of the biggest hosting providers in Sweden. However, since implementing this into the Detectify monitoring, we still find this at customers’ websites, showing that more service providers are vulnerable.

Let’s dig in and see how we managed to get around the mitigations.

Content type not being HTML

On the international hosting provider, the content would per default be text/plain which would only render the response as plain text. However, there is an old mod to Apache called Magic MIME that tries to figure out the content-type depending on the first bytes of the response. If the mod would be enabled, the content-type could be controlled depending on what type of characters the response would contain. For example <b> would lead to content type text/html and <?xml would lead to text/xml . When testing, a request to /.well-known/acme-challenge/<b> , the response actually came back as text/html .

A reference to Magic MIME was included in our report. However, the hosting provider politely came back explaining Apache wasn’t used, but that some form of middleware did the same form of content-type sniffing.

It was not possible to change the content type on the Swedish host provider. However, as Jan Kopecky showed in a blog post in April last year, it is possible to trick Internet Explorer into executing plain text as HTML, a trick that still works today in the latest version of Internet Explorer (it actually seems like Internet Explorer changed this behaviour prior to this post being published update 2: @filedescriptor informed it still works on Windows 8.1, but no longer on Windows 10).

This is done by creating a .eml-file and setting the content-type to message/rfc822 . It stands for Microsoft Outlook Express mail message and is used to save email content to a file. When loading such file, Internet Explorer will perform mime-sniffing (guessing content-type) of the rest of the content. As such, we can simply include a iFrame to the vulnerable endpoint and the content will be treated as HTML.

URL encoding request

When the request was made to the Swedish hosting provider, the content of KEY1 would always end up URL-encoded in the response.

Once again, we can use Internet Explorer to get past this issue. A not too known thing about Internet Explorer is that the search fragment (after ? in a URL) is actually by default not URL-encoded. In this case, everything after /.well-known/acme-challenge/ was written directly to the page, meaning /.well-known/acme-challenge/?<h1>hi generated a response with the proper HTML tag.

It is worth mentioning that it would be possible to do this even if only the pathname would be written to the page. If it does follow a redirect, Internet Explorer will leave this part non URL-encoded as well, meaning a PoC could be as simple as this:

<?php header(“Location: https://vulnerable/.well-known/acme-challenge/<h1>test”); ?>

XSS auditor

The very last thing before we can call this a day and send the bug report is making sure JavaScript does actually execute in the web browser. Firefox lacks an XSS-auditor, but as Chrome is widely used it would be nice to get it to work there as well to show the biggest kind of impact.

Remember that we can control the content-type. The Chrome XSS-auditor does not trigger on XML, however it is possible to include a XHTML-namespace that will evaluate the XML as HTML.

PoCs

A full PoC for the international provider would look like this:

/.well-known/acme-challenge/%3C%3fxml%20version=%221.0%22%3f%3E%3Cx:script%20xmlns:x=%22http://www.w3.org/1999/xhtml%22%3Ealert%28document.domain%26%23x29%3B%3C/x:script%3E

And for the Swedish provider, the PoC would look like this:

TESTEML Content-Type: text/html Content-Transfer-Encoding: quoted-printable <iframe src=3D"http://[redacted]/.well-known/acme-challenge/?<HTML><h1>meh</h1>"></iframe>

Mitigations

The key take-away here is that anti-patterns could sometimes lead to unexpected side effects and our recommendation is not to make the content from the acme-challenge request reflect at all. Instead, use the suggested method and only serve the response of KEY1.KEY2 if KEY1 is exactly the one being asked for and requested in the challenge.