Anozer Blog
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 theadmin'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
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 !
We perform the request to activate the templates
:
All that remains is to test if the template injection works :
And we retrieve the flag !
PWNME{de3P_pOL1uTi0n_cAn_B3_D3s7rUctIv3}