One of the most comfortable ways to build web pages is by using server-side templates. Such templates let you create HTML pages that include special elements that you can fill and modify dynamically. They are easy to understand for designers and easy to maintain for developers. There are many server-side template engines for different server-side languages and environments. One of them is Thymeleaf, which works with Java.
Server-side template injections (SSTI) are vulnerabilities that let the attacker inject code into such server-side templates. In simple terms, the attacker can introduce code that is actually processed by the server-side template. This may result in remote code execution (RCE), which is a very serious vulnerability. In many cases, such RCE happens in a sandbox environment provided by the template engine, but many times it is possible to escape this sandbox, which may let the attacker even take full control of the web server.
SSTI was initially researched by James Kettle and later by Emilio Pinna. However, neither of these authors included Thymeleaf in their SSTI research. Let’s see what RCE opportunities exist in this template engine.
Introduction to Thymeleaf
Thymeleaf is a modern server-side template engine for Java, based on XML/XHTML/HTML5 syntax. One of the core advantages of this engine is natural templating. This means that a Thymeleaf HTML template looks and works just like HTML. This is achieved mostly by using additional attributes in HTML tags. Here is an official example:
<table>
<thead>
<tr>
<th th:text="#{msgs.headers.name}">Name</th>
<th th:text="#{msgs.headers.price}">Price</th>
</tr>
</thead>
<tbody>
<tr th:each="prod: ${allProducts}">
<td th:text="${prod.name}">Oranges</td>
<td th:text="${#numbers.formatDecimal(prod.price, 1, 2)}">0.99</td>
</tr>
</tbody>
</table>
If you open a page with this code using a browser, you will see a filled table and all Thymeleaf-specific attributes will simply be skipped. However, when Thymeleaf processes this template, it replaces tag text with values passed to the template.
Hacking Thymeleaf
To attempt an SSTI in Thymeleaf, we first must understand expressions that appear in Thymeleaf attributes. Thymeleaf expressions can have the following types:
${...}
: Variable expressions – in practice, these are OGNL or Spring EL expressions.*{...}
: Selection expressions – similar to variable expressions but used for specific purposes.#{...}
: Message (i18n) expressions – used for internationalization.@{...}
: Link (URL) expressions – used to set correct URLs/paths in the application.~{...}
: Fragment expressions – they let you reuse parts of templates.
The most important expression type for an attempted SSTI is the first one: variable expressions. If the web application is based on Spring, Thymeleaf uses Spring EL. If not, Thymeleaf uses OGNL.
The typical test expression for SSTI is ${7*7}
. This expression works in Thymeleaf, too. If you want to achieve remote code execution, you can use one of the following test expressions:
- SpringEL:
${T(java.lang.Runtime).getRuntime().exec('calc')}
- OGNL:
${#rt = @java.lang.Runtime@getRuntime(),#rt.exec("calc")}
However, as we mentioned before, expressions only work in special Thymeleaf attributes. If it’s necessary to use an expression in a different location in the template, Thymeleaf supports expression inlining. To use this feature, you must put an expression within [[...]]
or [(...)]
(select one or the other depending on whether you need to escape special symbols). Therefore, a simple SSTI detection payload for Thymeleaf would be [[${7*7}]]
.
Chances that the above detection payload would work are, however, very low. SSTI vulnerabilities usually happen when a template is dynamically generated in the code. Thymeleaf, by default, doesn’t allow such dynamically generated templates and all templates must be created earlier. Therefore, if a developer wants to create a template from a string on the fly, they would need to create their own TemplateResolver. This is possible but happens very rarely.
A Dangerous Feature
If we take a deeper look into the documentation of the Thymeleaf template engine, we will find an interesting feature called expression preprocessing. Expressions placed between double underscores (__...__
) are preprocessed and the result of the preprocessing is used as part of the expression during regular processing. Here is an official example from Thymeleaf documentation:
#{selection.__${sel.code}__}
Thymelead first preprocesses ${sel.code}
. Then, it uses the result (in this example it is a stored value ALL) as part of a real expression evaluated later (#{selection.ALL}
).
This feature introduces a major potential for an SSTI vulnerability. If the attacker can control the content of the preprocessed value, they can execute an arbitrary expression. More precisely, it is a double-evaluation vulnerability, but this is hard to recognize using a black-box approach.
A Real-World Example of SSTI in Thymeleaf
PetClinic is an official demo application based on the Spring framework. It uses Thymeleaf as a template engine.
Most templates in this application reuse parts of the layout.html template, which includes a navigation bar. It has a special fragment (function), which generates the menu.
<li th:fragment="menuItem (path,active,title,glyph,text)" class="active" th:class="${active==menu ? 'active' : ''}">
<a th:href="@{__${path}__}" th:title="${title}">
As you can see, the application preprocesses ${path}
, which is then is used to set a correct link (@{}
). However, this value comes from other parts of the template:
<li th:replace="::menuItem ('/owners/find','owners','find owners','search','Find owners')">
Unfortunately, all the parameters are static and uncontrollable by the attacker.
However, if we try to access a route that does not exist, the application returns the error.html template, which also reuses this part of layout.html. In the case of an exception (and accessing a route that does not exist is an exception), Spring automatically adds variables to the current context (model attributes). One of these variables is path (others include timestamp, trace, message, and more).
The path variable is a path part (with no URL-decoding) of the URL of the current request. More importantly, this path is used inside the menuItem
fragment. Therefore, __${path}__
preprocesses the path from the request. And the attacker can control this path to achieve SSTI, and as a result of it, RCE.
As a simple test, we can send a request to http://petclinic/(7*7) and get 49 as the response.
However, despite this effect, we couldn’t find a way to achieve RCE in this situation when the application runs on Tomcat. This is because you need to use Spring EL, so you need to use ${}
. However, Tomcat does not allow { and } characters in the path without URL-encoding. And we cannot use encoding, because ${path}
returns the path without decoding. To prove these assumptions, we ran PetClinic on Jetty instead of Tomcat and achieved RCE because Jetty does not limit the use of { and } characters in the path:
http://localhost:8082/(${T(java.lang.Runtime).getRuntime().exec('calc')})
We had to use ( and ) characters because after preprocessing the @{}
expression receives a string starting with / (for example, /${7*7}
), so the expression is not treated as an expression. The @{}
expression allows you to add parameters to the URL by putting them in parentheses. We can misuse this feature to clear the context and get our expression executed.
Conclusion
Server-side template injection is much more of an issue than it appears to be because server-side templates are used more and more often. There are a lot of such template engines, and a lot of them remain unexploited yet but may introduce SSTI vulnerabilities if misused. There is a long way from ${7*7}
to achieving RCE but in many cases, as you can see, it is possible.
As security researchers, we always find it interesting to see how complex technologies clash and affect each other and how much still remains unexplored.
Get the latest content on web security
in your inbox each week.