Classic SSTI Vulnerability Demonstration

Classic SSTI Vulnerability Demonstration

This blog will contain the write-up for one of the challenges that I solved in the HTB Cyber Apocalypse CTF 2025: Tales from Eldoria.

Cyber Apocalypse CTF 2025 Banner

I hope that this writeup can also serve as a guide for some of you here who are new to SSTI Vulnerabilities, as I tried to simplify the explanation as much as possible.

⚠️
For educational purposes only! Do not attempt the demonstrated attacks on others without their permission.

Web: Trial by Fire

Very Easy | Server-side Template Injection (SSTI)

We are first given the ZIP file to assist us with capturing the flag.

A standard practice after a few CTFs I like to do when it comes to Web Challenges, as well as Rev(erse engineering) challenges, when they provide the source code, is to get an overview of the files in the folder.

Output of tree .

I often find vulnerabilities in Web CTFs in the routes.py file which I immediately turned my attention to. This file is pretty long; so to cut to the chase, this is where the SSTI Vulnerability resides.

Vulnerable portion of the code in routes.py

Long story short, variables like damage_dealt, damage_taken, spells_cast, etc..., are being pre-assigned (as if hard-coding) in the landing page that displays an input for players to enter a custom warrior name, as shown below.

Landing page where the page asks the player to input a warrior name.
Code in routes.py that handles the landing page

However, this leaves a vulnerability where I can make use of the template format utilized by Jjinja2 to inject malicious code into this input field.

We can confirm this by entering {{7*7}} into the Warrior Name input field and playing the game until we reach the battle report page.

Successfully exploited SSTI vulnerability

Because we lost the battle, we should have 0 health. However, because we injected {{7*7}}, the health points also returned 49.

Since we now know confirmed that the page is indeed vulnerable to SSTI, we can input payloads. However, I had to keep in mind that they restricted the warrior name to 30 characters maximum.

index.html HTML code for the input form, showing maxlength="30"

So, I injected {{config}} into the input field to find out what juicy contents it returns!

Battle Report after injecting {{config}}

Obviously, the part we would be interested in is the long SECRET_KEY value 56d3062e9674be1f05e62bf982cc0b7d2ff1b7a473019847af26146dae01ed5df28b1da72ce7cdefa018bf4eda0285f10f457213a671843e5afa64b48e6d35aa116a1e815e.

We would also need to note that this SECRET_KEY value is used in conjunction with creating the cookies for the page, which is done after the battle finishes, before the battle report is generated.

By using a Flask Session Cookie Decoder/Encoder, we can decode/modify the cookie to perform arbitrary SSTI commands as the command is baked into the cookie as shown in the next picture.

Now, lets decode the cookie to give us a better understanding of how this cookie was generated using a flask session cookie decoder. You can also use an online version to decode flask session cookies.

Decoded cookie

Here, we can see that the script returned the data that was used to generate the cookie. This means that we can encode a new cookie that makes use of the same SESSION_KEY and a new arbitrary Jjinja2 payload and the page will return whatever we asked for!

To cut to the chase, the following 2 images are the newly crafted arbitrary Jjinja2 payload and the output of the page.

Arbitrary Jjinja2 SSTI payload to run ls

Using this newly generated session cookie, we can replace the current session cookie with it.

Output of the arbitrary Jjinja2 SSTI payload

And we can see that the ls command successfully worked! Now, all we have to do is craft a payload that looks for flag.txt and use cat to view its contents.

In Linux, the command would be:
find / -name flag.txt -exec cat {} \; 2>/dev/null

So, this is the new Jjinja2 payload.

Generation of the new cookie using the new payload

And we will use this cookie to replace the current cookie which contains the previous SSTI payload, and we should find ourselves the flag!

Successfully obtained the flag!

Flag: HTB{Fl4m3_P34ks_Tr14l_Burn5_Br1ght_0f0712fc5de0b6df51407c2a1f12b8aa}

Flag Obtained!

~ The End ~

This specific challenge is of "Very Easy" difficulty, so in the context of real-world scenarios, companies and organisations would likely have already patched common vulnerabilities like the one demonstrated earlier.

In this SSTI vulnerability challenge, it could have been prevented by:

  • Implementing Input Sanitization (i.e. whitelist only necessary characters)
  • Use render_template() instead of render_template_string
  • Keep all packages/libraries used up-to-date

I hope you learned how SSTI vulnerabilities can be identified and exploited in the wild, as well as how to prevent and remediate such common vulnerabilities.

Happy Hunting!