Digital Overdose 2021 Autumn CTF Writeup — madlib (web)
Madlib is a CTF challenge in the web category with 250 points. The target was vulnerable to Server Side Template Injection therefore we can execute arbitrary commands on the target.
Actually, I did this challenge after the event has already ended because I was so obsessed with their OSINT challenges lol. I later noticed that the target instance can still be spawned and the flag can still be submitted as well. So, I decided to give it a try.
The challenge 🎈
Upon visiting the target, we get this simple page back:
If we submit some texts, the server will return a paragraph containing what we have submitted.
The author knows that we are lazy and provide us the source code, take a look at it:
Server Side Template Injection 💉
After skimming through the code, we could see that the input is not correctly handled. String interpolation is being used instead of template parameters. If we submit template syntax as an input, the template will be interpreted by the server. We can confirm this by submitting {{7 * '7'}}
. As the template gets interpreted, the server returns with 7777777
Normally, payload for SSTI will look similar to this:
{{ config.__class__.__init__.__globals__['os'].popen('ls').read() }}
However, we are limited to only 21 characters per field 😥
Workaround 💭
Observation
- We can make use of the
config
object in Flask. It is an object that stores the server’s configuration. This object has an interesting methodupdate
that we can use to store any variable in the configuration object. 🤔
config.update(key = value)
- Notice the dot in
{adjective}.{person}
. We can use this dot as part of the template, extending the length of our input! 🎉 - Jinja2, which is the template engine for Flask, support declaring variables inside the template with the syntax
{% set foo='bar' %}
😮
Idea
- Use variables in Jinja template to reduce the characters we needed to type. For an example, instead of
config.__class__
we{% set x=config %}
then usex.__class__
instead. 5 characters saved! - Of course, it is impossible to reach
popen('ls').read()
by doing #1 alone — we have only 5 input fields. Instead, we will store the variable in Flask’sconfig
. By doing so, we can recall the variable later and continue until the payload is complete.
Exploit 💥
1.
2.
3.
4. At this point, we have os.popen()
stored at config.a
So, we can now access the target’s terminal by calling the method.
5. Cool, let’s try listing the directory
6. This flag.txt should be our answer, let’s print it out
💣Boom~!
Bypass length limitation 🔐
Let’s explore some more even though we can just submit the flag and chill out.
We were kind of lucky that {{y('cat flag.txt')
fit in 21 characters. However, we are limited to commands within this length hence we cannot do much. If we could ignore this constraint, the target will be severely compromised.
Remember the technique we used to get from config
to popen()
?
We can do the exact thing with our command.
Jinja not only support variable inside template but also string concatenation with ~
(Taking the time to go read the documentation was really worth it)
We can concatenate our command part by part and store it in config
. Then we can just access it like config.command
Then
Now we have full command injection that grants us the opportunity for privilege escalation. 😈
Conclusion 📝
What went wrong
- User’s input must always be sanitized or at least sandboxed