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.

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.
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.

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.

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.


routes.py
that handles the landing pageHowever, 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.

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!

{{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.

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.

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

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.

And we will use this cookie to replace the current cookie which contains the previous SSTI payload, and we should find ourselves 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 ofrender_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!