skip to content
Back to GitHub.com
Home Bounties Research Advisories CodeQL Wall of Fame Get Involved Events
December 11, 2020

GHSL-2020-205: Remote Code Execution in Apache Struts 2 - S2-061 - CVE-2020-17530

Alvaro Munoz

Summary

Double evaluation of Struts tag dynamic attributes leads to Remote Code Execution

Product

Apache Struts 2

Tested Version

Latest commit to the date of reporting: 62424ef30c39167df493522286e8bb73d3fdcc4a

Details

Issue 1: Double evaluation of JSP tag’s dynamic attributes.

Struts performs a double or triple OGNL evaluation of dynamic attributes (those not defined in the TLD specification such as HTML5 data-* attributes).

Struts JSTL tags use FreeMarker templates to render the tag so the process normally involves three different layers:

  1. Tag classes (eg: org.apache.struts2.views.jsp.ui.AbstractUITag)
  2. Components (org.apache.struts2.components.UIBean)
  3. FTL templates

In the first step (AbstractUITag), dynamic attributes will be evaluated once by findValue:

    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);
        }
    }

In the second step, there are no additional evaluations and dynamic properties are just passed around as parameters.dynamicattributes.

In the third step, the component will merge and render the corresponding FTL template. For example <s:textfield> will use the text.ftl template:

<input<#rt/>
 type="${(parameters.type!"text")?html}"<#rt/>
 name="${(parameters.name!"")?html}"<#rt/>
<#if parameters.get("size")?has_content>
 size="${parameters.get("size")?html}"<#rt/>
</#if>
<#if parameters.maxlength?has_content>
 maxlength="${parameters.maxlength?html}"<#rt/>
</#if>
<#if parameters.nameValue??>
 value="${parameters.nameValue?html}"<#rt/>
</#if>
<#if parameters.disabled!false>
 disabled="disabled"<#rt/>
</#if>
<#if parameters.readonly!false>
 readonly="readonly"<#rt/>
</#if>
<#if parameters.tabindex?has_content>
 tabindex="${parameters.tabindex?html}"<#rt/>
</#if>
<#if parameters.id?has_content>
 id="${parameters.id?html}"<#rt/>
</#if>
<#include "/${parameters.templateDir}/${parameters.expandTheme}/css.ftl" />
<#if parameters.title?has_content>
 title="${parameters.title?html}"<#rt/>
</#if>
<#include "/${parameters.templateDir}/${parameters.expandTheme}/scripting-events.ftl" />
<#include "/${parameters.templateDir}/${parameters.expandTheme}/common-attributes.ftl" />
<#include "/${parameters.templateDir}/${parameters.expandTheme}/dynamic-attributes.ftl" />
/>

Right before closing the tag, it renders scripting, common and dynamic attributes. The last one being:

<#if (parameters.dynamicAttributes?? && parameters.dynamicAttributes?size > 0)><#rt/>
<#assign aKeys = parameters.dynamicAttributes.keySet()><#rt/>
<#list aKeys as aKey><#rt/>
  <#assign keyValue = parameters.dynamicAttributes.get(aKey)/>
  <#if keyValue?is_string>
      <#assign value = struts.translateVariables(keyValue)!keyValue/>
  <#else>
      <#assign value = keyValue?string/>
  </#if>
 ${aKey}="${value?html}"<#rt/>
</#list><#rt/>
</#if><#rt/>

The dynamic attributes are evaluated once again by translateVariables.

Therefore dynamic attributes are evaluated twice when OGNL forced evaluation is used (<s:textfield data-value="%{message}">): For example, for the case where message is an action property under user control, visiting the URL https:/foo.com/action?message%25{1%2b1} will result in a double evaluation:

If JSTL EL is enabled and the page uses ${} expressions (eg: <s:textfield data-value="${message}">), the attribute will get evaluated three times.

Visiting https:/foo.com/action?message=%25{%23parameters.foo[0]}&foo=%25{1%2b1} will result in the following evaluations:

We can tell it is a bug since there is either no evaluation when the attribute does not use delimiters (eg: data-value="message") or double/triple evaluation when it forces the evaluation with the delimiters (eg: data-value="%{message}")

Impact

This issue may lead to RCE.

Issue 2: Additional evaluation on FreeMarker tags

This issue is basically the same as S2-053 which describes a double evaluation where the first evaluation is a ${} FreeMarker evaluation in UnifiedCall and the second evaluation is an OGNL evaluation (normally in UIBean). When I verified the examples described in the security bulletin, both the insecure constructions (eg: <@s.hidden name="${message}"/>) and the secure proposed ones (eg: <@s.hidden name="%{message}"/>) were found to be vulnerable in 2.5.12 (first version where they should be fixed) and also in latest 2.5.22 version.

For example, given the following example that includes the insecure examples described in the bulletin:

<@s.hidden name="redirectUri" value=message />
<@s.hidden name="redirectUri" value="${message}" />
<@s.hidden name="${message}"/>

We can trigger double evaluations by providing %{} delimited expressions, eg: http://localhost:8080/test?message=%25%7B1%2b1%7D (%{1+1}) even in the latest versions of Struts. Therefore S2-053 fix doesn’t seem to be working as intended.

The examples proposed as secure alternatives in the same bulletin are also vulnerable:

<@s.hidden name="redirectUri" value="%{message}" />
<@s.hidden name="%{message}"/>

We can trigger double evaluations by providing NO delimited expressions, eg: http://localhost:8080/test?message=1%2b1 1+1

Please note that these are much more critical than the JSP tag ones (S2-059) since in this case all OGNL evaluations are evaluated an additional time. Thats it, single evaluations such as the value attribute ones are now evaluated twice, and double evaluations such as the id or name ones are now evaluated three times!

name/value attribute double evaluation is the most concerning one since they may contain user-controlled data as in:

Change your username: <@s.textfield value="${username}"/>

or with %{} delimiters:

Change your username: <@s.textfield name="%{username}"/>

The recommendations from S2-053 suggest using %{} expressions rather than ${} but since the latter are the ones that developers use in FTL templates, it is error-prone and may easily lead to critical bugs.

Impact

This issue may lead to RCE.

Issue 3: Additional evaluation on Velocity tags

Similarly to the issue described above, When using ${} delimited expressions, Velocity templates also perform an additional evaluation to the ones performed by the Component and FTL template layers.

Velocity Struts tags are implemented as Velocity Directives and all of them extend from org.apache.struts2.views.velocity.components.AbstractDirective. They all work in a similar way where tag parameters are passed in the form of key=value strings, eg:

#stextfield ("label=First name" "name=John")

Directive parameters are parsed by AbstractDirective render method and then added as tag parameters by putProperty). In this method, the directive parameters are evaluated before splitting them by =. This means that there is an additional evaluation to the ones performed later by the Component and FTL template layers which causes simple evaluated attributes to be evaluated twice. Eg:

#stextfield ("label=First name" "value=${username}")

Note that even if the value attribute is only evaluated once when using JSP tags, in the case above, there will still be a second evaluation.

Impact

This issue may lead to RCE.

CVE

CVE-2020-17530

Coordinated Disclosure Timeline

Resources

Contact

You can contact the GHSL team at securitylab@github.com, please include a reference to GHSL-2020-205 in any communication regarding this issue.