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:
- Tag classes (eg:
org.apache.struts2.views.jsp.ui.AbstractUITag
) - Components (
org.apache.struts2.components.UIBean
) - 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:
AbstractUITag.java
:%{message}
->%{1+1}
dynamic-attributes.ftl
:%{1+1}
->2
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:
- JSTL evaluation:
${message}
->%{parameters.foo[0]}
(This is possible since dynamic attributes are not declared in the TLD and therefore they lack thertexprvalue=false
value) AbstractUITag.java
:%{#parameters.foo[0]}
->%{1+1}
dynamic-attributes.ftl
:%{1+1}
->2
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
- 08/28/2020: Initial report sent to Apache Security Team
- 10/13/2020: Issue is acknowledged.
- 11/07/2020: A first patch is shared for evaluation.
- 11/11/2020: A second patch is shared addressing a missed case.
- 11/13/2020: A new patch is proposed to address new RCE payloads in the packages blocklist.
- 12/08/2020: Fix is released as part of 2.5.26
Credit
This issue was discovered and reported by GHSL team member @pwntester (Alvaro Muñoz).
Resources
- https://struts.apache.org/announce#a20201208
- https://cwiki.apache.org/confluence/display/WW/S2-061
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.