scroll it

Exploits Explained: Second Order XXE Exploitation

0% read

Kuldeep Pandya is a member of the Synack Red Team. You can find him on Twitter or his blog.

This writeup is about my recent discovery of a Second Order XXE that allowed me to read files stored on the web server.

One morning, a fresh target was onboarded and I hopped onto it as soon as I received the email. In the scope, there were two web applications listed along with two postman collections. I prefer postman collections over web apps, so I loaded the collections with their environments into my postman.

After sending the very first request, I noticed that the application was using SOAP API to transfer the data. I tried to perform XXE in the SOAP body but the application threw an error saying “DOCTYPE is not allowed”.

Here, we cannot perform XXE as DOCTYPE is explicitly blocked.

Upon checking all the modules one by one, I came across a module named NormalTextRepository in the postman collection which had the following two requests:

  • saveNormalText
  • GetNamedNormalText

After sending the first saveNormalText request and intercepting it in Burp Suite, I found out that it contained some HTML-encoded data that looked like this:

Upon decoding, the data looked like this:

<?xml version="1.0"?>
<normal xmlns="urn:hl7-org:v3" xmlns:XX=""><content XX:statu

This quickly caught my attention. This was XML data being passed inside the XML body in a SOAP request (Inception vibes).

I went on to try XXE here as well. For this, I copy pasted a simple Blind XXE payload from PortSwigger:

<!DOCTYPE foo [ <!ENTITY % xxe SYSTEM ""> %xxe; ]>

I used Synack’s provided web server to test for this. Upon checking its logs, I found there indeed was a hit for the /XXETEST endpoint.

This still was a blind XXE and I had to turn it into a full XXE in order to receive a full payout. I tried different file read payloads from PayloadsAllTheThings and HackTricks but they did not seem to work in my case.

For me, the XXE was not reflected anywhere in the response. This is why it was comparatively difficult to exploit.

After poking for a while, I gave up with the idea of full XXE and went ahead to check if an internal port scan was possible or not as I was able to send HTTP requests.

I sent the request to Burp Suite’s intruder and fuzzed for the ports from 1 to 1000. The payload for that looked like the following:

<!DOCTYPE foo [ <!ENTITY % xxe SYSTEM "§1§/XXETEST"> %xxe; ]>

However, the result of the intruder didn’t make any sense to me. All the ports that I fuzzed were throwing random time delays.

I lost all hope and was about to give up on this XXE once again. Then a thought struck, “If this data is being saved in the application, it has to be retrievable in some way as well.” I checked the other GetNamedNormalText request in this module and instantly felt silly. This request retrieved the data that we saved from the first saveNormalText request.

I used the following XXE file read payload and saved the data:

<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE foo [<!ENTITY example SYSTEM "/etc/passwd"> ]>

Then sent the second GetNamedNormalText request to retrieve the saved data. And in the response, I could see the contents of the /etc/passwd file!

This was enough for a proof of concept. However, looking at the JSESSIONCOOKIE, I could tell that the application was built using Java. And, in Java applications, if you just provide a directory instead of a file, it will list down the contents of that directory and return it.

To confirm this theory, I just removed the /passwd portion from the above file read payload. The updated payload looked like this:

<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE foo [<!ENTITY example SYSTEM "/etc"> ]>

Upon saving the above payload and retrieving it using the second request, we could see the directory listing of the /etc directory!

I sent it to Synack and they happily triaged it within approximately 2 hours.