During our CVE-2018-11776 research, after building 115 versions of Apache Struts, we had to address the challenges of recreating the execution environments.
In August 2019, the Synopsys Cybersecurity Research Center (CyRC) coordinated with the Apache Software Foundation to publish Apache Struts Security Advisory S2-058. The advisory represents research undertaken in Belfast that focused on 64 vulnerabilities through 115 versions of Struts, identifying roughly 50 affected versions per vulnerability. We wanted to share our experiences in a series of blog posts.
This blog series is for a technical audience. It discusses insights, problems we encountered, and solutions we came up with during the project:
This is the second blog post in the series. We recommend looking at the first post if you haven’t had a chance.
During August 2018, we examined a newly released Apache Struts remote code execution vulnerability (CVE-2018-11776 / S2-057). Through creating our own proof-of-concept and testing it against Apache Struts’ past releases, we discovered that the vulnerability affected more versions than were initially reported. We reported these findings in accordance with our responsible disclosure policy. But our discovery also prompted the question, what about all the previous Apache Struts vulnerabilities? We set out to create a system where we could conduct vulnerability research at scale.
Java was one of the first languages to promote the idea of universal binaries and components, or “build once, execute everywhere.” It was meant to provide a stable platform for development and execution. However, Java is also quite old and has seen many changes over the years. Java platforms have different nuances, and in the case of Struts 2, there is no single servlet runner either.
Apache Struts 2.0.1, the first version of Struts 2, was released over a decade ago in September 2006, when Java 1.5 (which still supported older operating systems, such as Windows 95) was still king. (It doesn’t that seem deprecating older operating systems is any easier now, as Windows 7, released in 2009, left support only recently.)
Rather than reproduce every single environment that existed previously, we decided to take a simpler approach in the beginning, trying a reasonable in-between Java runtime and servlet runner and seeing where our edge cases lay.
We started with a quick and dirty approach for running our different versions of Struts. We had set up a basic Tomcat installation on Ubuntu 18 LTS using the system’s provided Tomcat 8 and Java 8 packages. Java 8 was capable of compiling for 1.5, our starting point. And Tomcat 8 was known to be backward compatible back to the 1996 Java servlet 1.0 specification, which was far older than what we expected from the Struts 2 WAR files. Also, it was modern enough that in theory, many later versions of Struts could run in the same instance.
Surprisingly, it’s quick and trivial to set this up on Ubuntu 18. It requires only a few chained commands, and within a few minutes, you’ll have our initial configuration running:
|sudo apt-get update && sudo apt-get -y dist-upgrade && sudo apt-get -y install openjdk-8-jdk tomcat8 tomcat8-user && sudo service tomcat8 stop && sudo update-java-alternatives -s java-1.8.0-openjdk-amd64 && sudo service tomcat8 start|
Many of the WARs we prepared ran without error or issues, as expected. We often used the showcase WAR file initially to test some of the basic Struts functionality first.
When reproducing exploits, or creating new exploits based on known information, we ran into a variety of barriers, which we will detail below.
This vulnerability allows a denial-of-service (DoS) via an insufficient blacklist in CookieInterceptor leading to ClassLoader manipulation.
When we started sending payloads to the server, we noticed with remote debugging that the payload was not triggering anything. When we looked closer for the payload’s existence, we couldn’t find it at all in our remote debugging tools. It was also returning a Tomcat 404 error message.
Considering the payload, we didn’t expect this to be a problem produced by the environment. However, it eventually became clear that something was sanitizing the data before it was passed to the servlet. Even more curious, when we checked the access log produced by Tomcat, we saw that the accessed URL was different from what we had expected.
|Tomcat 8 access log|
|“GET /j8-22.214.171.124/showcase.action HTTP/1.1” 404 989|
We tried switching to Tomcat 6.5 and reran the exploit. We discovered that the payloads functioned properly. Further, the Tomcat 6.5 access log also looked very different.
|Tomcat 6.5 access log|
|“GET /j8-126.96.36.199/showcase.action?Class.ClassLoader.resources.dirContext.docBase=%2Ftmp HTTP/1.0” 200 12502|
This is a good example of how far you can go toward mitigating vulnerabilities simply by keeping infrastructure around older software upgraded. On the reverse side, it might mean that exploits like these exist in other products but go undetected because the average install is expected to be running more modern infrastructure.
This vulnerability allows OGNL evaluation on ExceptionDelegator. OGNL expressions are Turing complete and often have access to base Java classes, which attackers can further use to execute code within the context and capabilities of the host application, leading to remote code execution (RCE).
This vulnerability introduced a payload through the cookie header. What was interesting about this vulnerability was that the inclusion of Apache Struts was enough to make it exploitable, so even the blank.war application was susceptible. In our example payload, we decided just to launch a shell command using OGNL and touch a file:
|HTTP Payload exchange|
|GET /blank_j8-2.3.1/example/HelloWorld.action HTTP/1.1 Host: localhost User-Agent: Synopsys-CyRC/2019 Cookie: (#_memberAccess[“allowStaticMethodAccess”]\u003dnew\u0020java.lang.Boolean(true))(x)=1; x[@java.lang.Runtime@getRuntime().exec(“touch@/tmp/RCE”.split(“@”))]=1|
|HTTP/1.1 200 OK Server: Apache-Coyote/1.1 Set-Cookie: JSESSIONID=52154AE434607CD570E5280CBD321A19; Path=/blank_j8-2.3.1 Content-Type: text/html;charset=UTF-8 Content-Length: 534|
This worked and produced /tmp/RCE when it came to Tomcat 6. However, when we tried this same payload on Tomcat 8, it was quick to sanitize the cookie input and declare its displeasure in status logs:
|Tomcat 8 status log – failed attempt|
|org.apache.tomcat.util.http.parser.Cookie.logInvalidHeader A cookie header was received [(#_memberAccess[“allowStaticMethodAccess”]\u003dnew\u0020java.lang.Boolean(true))(x)=1; x[@java.lang.Runtime@getRuntime().exec(“touch@/tmp/CVE-2012-0392/2.3.1”.split(“@”))]=1] that contained an invalid cookie. That cookie will be ignored.Note: further occurrences of this error will be logged at DEBUG level.|
Again, this shows that keeping infrastructure around older software upgraded can go a long way toward mitigating vulnerabilities.
This vulnerability allows an RCE attack when using the Struts REST plugin with XStream handler to parse XML.
|Java Runtime Environment 8 vulnerability 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|
When we tried these same exploits against Java Runtime Environment 11 and a variety of others, we discovered that the payloads did not work. This was in part due to changes that occurred in runtime distributions, which lead to a com.thoughtworks.xstream.mapper.CannotResolveClassException.
However, we were able to modify the payload further so it did function in 11. Our experience shows that there are cases where a vulnerability seems mitigated but further modifications can be made to create an exploitation. In this case, it wasn’t enough just to upgrade the environment around the vulnerable component to mitigate it.
In our next blog post about exploitation, we’ll go into more depth about why it is necessary to modify exploits and similar scenarios to this.
The error pages now describe S2-006 as a cross-site scripting (XSS) vulnerability, thanks to this project.
S2-006 was originally published with basic information that didn’t make much sense:
|S2-006 vulnerability summary|
|Cross site scripting (XSS) vulnerability on <s:url> and <s:a> tags
For both the <s:url> and the <s:a> tag, it is possible to inject parameter values that do not get escaped properly when the tag’s resulting URLs are constructed and rendered. The following scenarios are known:
Through a lot of trial and error, we eventually learned that these vulnerable tags were generated by error pages when Struts was running in development mode. In development mode, Apache Struts generates its own error pages. Otherwise, Apache Tomcat generates error pages, which aren’t susceptible to these issues.
Environments matter, even in Java, where environments are usually very compatible. Runtime version, server software, and even software configuration all have an effect. Surprisingly, new runtimes and servlet software can mitigate some issues. In other cases, they can simply break an exploit, which requires an alternate payload to work around the issue.