If you’re active in the cybersecurity industry, you have likely heard the buzz about Struts 2 Java framework in 2017. In short, hackers were able to exploit a vulnerable application based on Struts 2 and stole hundreds of millions of PII records.
The vulnerability (CVE-2017-5638) made a lot of noise, but like almost any critical vulnerability, it was patched and everyone moved on. However, this ultimately ended up being the tip of the iceberg since there are other, lesser-known vulnerabilities with equally-serious consequences. The root cause of these vulnerabilities are still there as a result, and even the latest patch does not guarantee the complete safety of a Struts 2-based application.
In this post, I will show you why Struts 2 is a dangerous toy to play with and where to poke to break it.
In the graphic below, the fourth column from the left shows the number of code execution vulnerabilities in Struts, accounting for nearly 33% of the issues discovered. No other Java framework has this many. What’s more, all of them share the same root cause—the framework’s architecture itself. Let’s take a closer look.
Source: CVE Details
Struts Architecture
There are two major architectural solutions stitching together model, views, and controllers: Value Stack and OGNL. Value Stack is the stack of all the objects used by an application to respond to a user request (e.g. application configuration, security settings, data, etc.). Objects in the Value Stack are manipulated using Object Graph Navigation Language (OGNL), which is an expression language handy for getting and setting Java object properties.
OGNL
What is the OGNL library capable of? With the following example, OGNL calls a method of a given class, as well as doing something worse.
As you can see, an OGNL expression can contain an operating system command (e.g., cmd.exe, calc.exe, etc.). Therefore, if you allow the execution of malicious input as OGNL expressions, then you are in trouble.
Luckily, Struts 2 doesn’t expose OGNL directly. It has several layers above it a shown below.
At the top of the pyramid resides an entrance point to the OGNL expression. The example below shows a view that retrieves the username from the session object using “#session.user.name” OGNL expression. It is an intentional and legitimate way of using OGNL in Struts 2. However, malicious data may be evaluated as an OGNL expression unintentionally because of a vulnerability in the application or framework itself.
The next layer of the pyramid is the framework’s implementation of OGNL library. The most interesting part for us is the security mechanism because it defines which OGNL expressions are allowed to be evaluated.
Finally, after passing security checks, the expression is evaluated by Java OGNL library.
Recipe of a Successful Attack
Therefore, you need two key components to evaluate arbitrary OGNL expressions in a Struts 2-based application:
- Find an injection point leading to the evaluation
- Bypass the security mechanism
Injection Points
How can a malicious OGNL expression be evaluated? There are two options:
The direct evaluation method happens when the user input ends in an argument of a method that evaluates the input as an OGNL expression. The following framework code took part in the notorious CVE-5638-2017 by evaluating an error message tainted with the malicious OGNL expression.
The double evaluation method consists of two evaluation calls, making it harder to find. As demonstrated in the following example, the first call populates a variable with a malicious expression but does not evaluate it yet. Meanwhile, the second call gets the tainted variable value and evaluates it. The double evaluation is usually unintentional. For example, it may combine the evaluation introduced by a developer and the evaluation inside the framework code.
Security Mechanism Bypasses
Now that you know what injection points look like, let us imagine that the payload is evaluated, allowing us to play with the security mechanism.
You will see its evolution from an attacker perspective starting from Struts 2 version 2.3.14.1 (2013) up to the current state in versions 2.5.20 or 2.3.37. The security mechanism in versions before 2.3.14.1 is trivial; afterward, it was continuously upgraded.
Now comes the best part where we are going to review the payloads that bypass the security mechanism and upgrades made against them. The general approach of all the following payloads is to first remove the existing restrictions, and then run an OS command.
The first payload
The security mechanism in the initial state restricted access to static method calls. For some reason, the mechanism itself was accessible to OGNL expressions. The following payload first permits static method calls, and then calls a static method getRuntime().exec with the OS command.
The fix in 2.3.14.2 made allowStaticMethodAccess immutable. What if instead of a static method, we try to generate an object dynamically? The next payload does exactly this to bypass the security mechanism.
The second payload
The fix in 2.3.20 prohibits usage of constructors and introduces blacklists of classes and packages restricted for OGNL.
The third payload
The security mechanism disallows static methods, but allows static objects. The researchers found a static object in the OGNL library containing the security mechanism in its default state (= zero security settings). The following payload sets the current security mechanism to the default state and runs a command.
The fix in 2.3.30 and 2.5.2 finally deprived OGNL expressions of access to the security mechanism by blacklisting ognl.MemberAccess and ognl.DefaultMemberAccess classes.
The fourth payload
OGNL can call methods of any object in the given context. It occurred that the blacklists are in the context of an evaluated expression, which can be reached through a chain of several objects and flushed using clear() method. When blacklists are empty, we can use the trick from the previous payload to run a command.
The fix in 2.3.32 and 2.5.10.1 blocked the path to OgnlUtils by making com.opensymphony.xwork2.ActionContext unavailable for OGNL. In addition, blacklists were made immutable, making clear() method ineffective.
The fifth payload
ValueStack is a complex set of intertwined objects where you can reach the same object in several ways, starting from the Value Stack root. The next payload accesses OgnlUtils through a different set of objects. Blacklists were made immutable incorrectly by the previous fix and they can be updated using a setter. The payload consists of two consequent requests, the first of which request flushes blacklists, and the second of which runs a command. It’s possible to break the payload into two requests because OgnlUtils is a singleton and keeps its value until the application restart.
No More CVEs, but the Vulnerabilities May Still Be There
The fix in 2.3.35 and 2.5.17 fixed the flaws and put Struts 2 OGNL injections on pause. The blacklists are destined to be bypassed, so we can wait for the next episode of the cat and mouse game between the framework developers and attackers.
Currently, the framework does not have known injection points and code execution exploits. However, it does not make the Struts 2-based application safe because developers may introduce an injection point themselves. For example, this can occur by unintentionally chaining a benign evaluation inside framework code, with one more evaluation in their own code. The following code snippet is an example of how the application based on the latest version of Struts 2 is still exploitable when there is an injection point.
At first, the “createUser” action creates an object of class User and saves it to the session variable. The default value of the “isAdmin” property of the “User” class is false.
Later, the “inject” action loads the “User” object from the session, evaluates user input as OGNL expression, and saves the “User” object back to the session. The “getText()” method is an injection point inserted for demonstration purposes.
By calling these actions sequentially, the attackers can manipulate non-blacklisted objects. For example, set “User.isAdmin” to true, thus bypassing an authorization check.
Given these points, Struts 2 developers should keep yet another vulnerability type in mind. The recommendation is to use SAST or IAST tools like Checkmarx extensively, to find possible code execution via OGNL injection vulnerabilities. Without proper application security testing processes in place, the likelihood of overlooking an exploitable injection vulnerability caused by a developer is quite high.