Making Struts2 App More Secure: Disable Dynamic Method Invocation

Posted by Jon, Comments

(Part 1 of this series is available here.)

Overview

Struts2 has a very interesting feature called Dynamic Method Invocation:

Dynamic Method Invocation (DMI) will use the string following a "!" character in an action name as the name of a method to invoke (instead of execute). A reference to "Category!create.action", says to use the "Category" action mapping, but call the create method instead.

To expand on the above, given a Struts2 Action (its name for a controller) and dynamic method invocation is enabled in the application, then any public zero-argument method on the Action or its super classes (all the way to java.lang.Object!) can be called remotely.

For the cheap seats: any public zero-argument method on the Action or its super classes (all the way to java.lang.Object!) can be called remotely.

Where can this go wrong?

Ok, so some methods are exposed. "This isn't a big deal" I hear you thinking. (I got decent hearing...) Well, not so fast. Here are a couple of areas where DMI can cause serious issues:

  • Calling non-idempotent methods
  • Leaking sensitive data
  • Controlling view resolution
  • Shenanigans

Non-idempotent Methods

In a previous life, I blogged about Struts2 that indirectly touched on DMI. The gist of the blog was that certain methods that changed state (like prepare()) could be called in any arbitrary sequence. The risk here is somewhat tempered in that the attacker would have to know, infer, or detect the underlying business logic. Once known, the attacker could possibly circumvent business logic constraints by calling these methods out of order. Bad, sure. But not the end of the world.

Leaking Sensitive Data

When a controller method returns a string in Struts2, that string is looked up in a pre-defined set of results. If the return value matches a result, then usually Struts2 dispatches to a view. However, if the view isn't found, Struts2 may reflect the value back to a user via an error / exception. Derp.

Controlling View Resolution

Now, thinking about the above statement, if the attacker controls the return value, then the attacker can dispatch to whatever view he or she desires. This may circumvent business logic, disclose unauthorized data via the view display, or a bunch of other shenanigans. Out of the three above, this is also the easiest to initially test because of how Struts2 passed tainted data into the application and since Struts2 has a bunch of default return values:

  • success
  • none
  • error
  • input
  • login

But how does someone control the input?

Struts2 passes tainted data from its entry points using a couple different conventions. One convention is via the JavaBean convention of getters and setters on the controller itself. (Another is via ModelDriven.) That is, if the controller has a public method starting with set and receives a java.lang.String parameter, its value can be set via the controller as a parameter (GET or POST).

Here's a snippet from a sample tutorial by @mkyong:

package com.mkyong.user.action;

public class WelcomeUserAction{

    private String username;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    // all struts logic here
    public String execute() {

        return "SUCCESS";   // Jon here, while this seems like a typo (should be 
                            // "success"), it uses "SUCCESS" in the result mapping...

    }
}

(Note: I haven't checked to see if the tutorial enables or disables DMI. I'm assuming it's enabled. Just follow along anywho, enabling it if it's disabled.)

This controller / Action happens to be exposed via the URL /User/Welcome.do. To call getUsername, one would use the following URL: /User/Welcome!username.do. And to set the username value, just slap on a username parameter like so: /User/Welcome!username.do?username=SUCCESS. This sets the username field on the Action via the setUsername(java.lang.String) method, calls the getUsername() method, which now returns the just-set field with the value of SUCCESS, and then the view is resolved via Struts2 view resolution. Whoop!

Shenanigans

Seriously, why should I be able call Object.notifyAll() via /User/Welcome!notifyAll.do ?!

How to disable!

Refer to the Struts2 DMI documentation. The safest way is to set struts.enable.DynamicMethodInvocation to false in the application configuration. The application configuration can be a .properties file or an XML configuration, both of which have different syntax:

Properties Syntax

struts.enable.DynamicMethodInvocation=false

XML Syntax

<constant name="struts.enable.DynamicMethodInvocation" value="false" />

Their page also mentions ways to filter allowed methods. If the application is legacy, then it might not be possible to disable DMI if ! is hardcoded into a lot of the application. So, this approach may make sense to mitigate the risk.

Comments!