In code we Struts: in-depth review of a Java vulnerability for developers

Houston, we have a security issue!

Alas, most public coverage of a severe issue is in the lines of “OMG, there is a critical issue you should update ASAP otherwise kittens will die, and your soul will burn in hell”.

If you do not have a soul nor care about kittens, simply replace kittens with “applications in production” and “soul burning in hell” with “being called by the CEO at 4 am during vacation”.

This article aims to:
– Help you reproduce CVE-2017-5638.
– Understand the technical issue(s) involved, without being a security expert.

I’ve personally only worked once for a few days on a Struts2 project, thus being familiar with Struts is not a requirement.

When a vulnerability is reported, the first questions that arise are:
– Am I really impacted? should I care about this issue?
– If impacted, what could happen if this vulnerability is exploited?

First, let’s start by reading the issue report in the CVE database.

Security vulnerabilities are reported in a central database called CVE for Common Vulnerability Enumeration which acts as a reference for security issues.

The first thing to look for in the report is the CVSS for Common Vulnerability Severity Score, which gives an idea of the issue’s impact. CVSS is on a scale of 10, the higher the value, the more impact it has. This score is calculated from vulnerability properties using a CVSS calculator.

Here we have a CVSS of 10, and the vulnerability allows an attacker to perform a Remote Code Execution (RCE). In plain English, it means that an attacker can make a vulnerable application execute any code on the application server with the same access level of application. For example, an attacker could open a reverse shell (also called Shell shoveling) and get a console on a remote machine without requiring advanced hacking skills.

Of course, when a vulnerability is found in a software component, users of that said component should update swiftly to avoid staying vulnerable. There are some cases where updating is easier said than done, for example, if you rely on an old unsupported version of the library (hello technical debt!) or if your users require manual action to update their product.

Analysis

From the CVE and Struts issues descriptions S2-045 & S2-046
– 2.5.10 version is affected
– 2.5.10.1 is not affected and contains the fix
– Is somehow related to errors and file upload
– Is a Remote Code Execution (RCE) vulnerability

Doing a quick diff between those versions enables us to see how it was fixed.

At this point, you should be able to assess more precisely if you’re impacted or not. For example, if you enable verbose classloading and the fixed classes aren’t loaded after running all your application code for a while, you can say that you aren’t affected by this issue.

Going back to code, only a few lines have been changed in the FileUploadInterceptor.intercept(…) method. At first sight, nothing is obvious, it is the kind of issue that could easily pass code reviews without being noticed.

There is a metaspoilt module available, which means we could exploit the vulnerability straight away without trying to understand it. It could be fun, but won’t teach you anything apart from blindly re-using others’ work. Having an exploit available means that any script kiddie can try it in the wild.

Star Wars, Yoda - It's not the destination but in the journey happiness is

However, we will still use it as an aid to make our path easier. Finding such vulnerabilities is a full-time job. It requires time, resources and knowledge that are far beyond what we could expect from mortal developers like us.

In other words, with a pre-made exploit, you get a usable rare planet alignment but you lose the ability to sharpen your astronomy skills.

Enough small talk, show me the code!

As non-Struts-experts, we’ll start using an existing sample application.
Here, the “file-upload” example available in official struts examples repository seems appropriate.
You will need to have git, maven and a JDK properly installed to proceed.


git clone https://github.com/apache/struts-examples cd struts-examples/file-upload

Override struts2.version property in pom.xml by adding this in <properties> tag:

<struts2.version>2.5.10</struts2.version>

Then build the app as any maven app: mvn clean install

This sample application can be run using a Jetty plugin: mvn jetty:run.
You should be able to open http://localhost:8080/file-upload/ and try it.

If we change the Struts version from 2.5.10 to 2.5.10.1, we can see using mvn dependency:tree that the only dependency that changes is struts-core, which is consistent with the fixed code.

There are two parts to this issue:
– A crafted HTTP header gets injected in OGNL parser
– Expression injected in OGNL enables to execute code

OGNL is an expression-based language that is used in Struts to access object properties in HTML templates, and also happen to enable code execution in some cases.

The vulnerability comes from those two parts communicating together, a value provided by the user (thus untrusted) is being injected directly into the OGNL parser, thus allowing remote code execution.

Step one: inject header and reach OGNL evaluation

At this step, we want to find the conditions to reach the method call that has been removed by the fix: LocalizedTextUtil.findText(…) within the intercept(…) method.

The most convenient way to find such conditions is to debug the application with your favorite IDE:
– Restart application in debug mode using mvnDebug jetty:run (will listen on port 8000 by default)
– Import project code into your IDE, and start a remote debug session on port 8000.
– Open FileUploadInterceptor class and ask your IDE to download Struts source code
– Put some breakpoints within the intercept(...) method.
– Trigger some file uploads in-app, find conditions that enable to execute findText(...).

Then, after some trial and error, you should find that both conditions should be met:
– You have to upload multiple files, otherwise, it’s not a multipart upload
– You need to have some errors: MultiPartRequestWrapper.getErrors() is not empty, it delegates to MultipartRequest to retrieve errors.

There are two implementations of MultipartRequest available, and only one that adds errors: it’s within JakartaMultiPartRequest.parse(...).

By luck (or lack thereof), when we try to inject an OGNL expression into the HTTP request header, an error is triggered because the Content-Type header value is made invalid.

For convenience, here is the simple bash script I used to trigger attacks using curl. The header value have been inspired by the exploit but does not try to reproduce the RCE.

#!/bin/bash
header="%{('multipart/form-data')}"

file1=$(tempfile)
file2=$(tempfile)

# we have to upload multiple files
curl http://localhost:8080/file-upload/upload.action \
-F upload=@${file1} \
-F upload=@${file2} \
-v \
-H "Content-type: ${header}" \

rm ${file1}
rm ${file2}

An error is triggered server-side when we try to execute this request. In debug we can now see that the LocalizedTextUtil.findText(...) method call is reached.

defaultMessage parameter value contains the injected header value

We can now see that the defaultMessage parameter value contains the injected header value as-is. Congratulations! We finally reached the end of the first step! If you read Javadoc comments on this method, you’ll see that it mentions OGNL expression evaluation whenever a localization key is not found.

Step two: trigger code execution in OGNL

From previous step, we know that LocalizedTextUtil.findText(...) with an OGNL expression as defaultMessage parameter value triggers OGNL expression evaluation. Thus, we just have to find an expression that is able to execute code.

The most convenient way I found to experiment with OGNL expressions quickly is to add a unit test to the project. Reading available OGNL exploits should give us enough inspiration to find what we are looking for.

OGNL evaluation requires some execution context in order to resolve variables and objects. By digging into Struts source code I found that extending XWorkTestCase provides most of the execution context.

The OgnlUtil class provides some safety checks through setAllowStaticMethodAccess and setExcludedXXX methods to prevent some known methods and classes from being executed. However, since those collections are mutable, any code having access to an OgnlUtil instance can easily disable them.

Now, we just have to plug everything and we can craft our own attacks using any basic http client like curl. See https://gist.github.com/SylvainJuge/cd1b5c875ed27e6374e63caa550af813 for examples.

What did we learn from this?

In hindsight, once you understand how it works it seems pretty obvious. However, many security issues happen because multiple things that were made apart (and probably never meant to be together) got plugged-in together in an unexpected way. As developers, we should always stay humble and admit that we are just unable to accurately handle all corner cases.

The way it has been fixed by changing a few lines is enough to avoid the vulnerability in the short term. However, there is no way to make sure it won’t happen again. If Struts (or any web framework) was Superman, OGNL would probably be Kryptonite, keeping them close together is always risky.

Then, there are some security features of OGNL parser, but they have been easily disabled here. Using a whitelist is probably a simple and effective way to avoid it, but some expressions allow to disable it. Making this configuration immutable would have prevented most of the attack here, it’s exactly the kind of things that any reader of the Effective Java book by Josh Bloch would have noticed. If you haven’t read this book, stop reading here and grab a copy.

This post is probably long enough to be qualified as a short novel, I hope you enjoyed this quick introduction to the issue that made Equifax famous for its security policy.

Ensuring application security is always a moving target, this article has been written more than one year after the vulnerability was discovered and the list of known vulnerabilities of this Struts version is now a lot longer: CVE-2017-12611, CVE-2017-9804, CVE-2017-9805, CVE-2017-9787, CVE-2017-5638, CVE-2018-1327, CVE-2017-15707, CVE-2017-7672, CVE-2017-9793.

About the author:

Sylvain is a software engineer at Sqreen. He loves deleting code, vintage Java applications archeology and cheese.