Gestion des sessions sous Flask

Flask est un framework Python permettant de développer des applications web. Au cours d'un challenge proposé par la plateforme Hack The Box j'ai eu à faire à un cas que je n'avais jamais encore vu. La page d'accueil du challenge est la suivante:

Après avoir lancé un rapide dirsearch on tombe sur la page /debug dont voici le contenu:

from flask import Flask, Response, session, render_template
import functools, random, string, os, re

app = Flask(__name__)
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'tlci0GhK8n5A18K1GTx6KPwfYjuuftWw')

def calc(recipe):
    global garage
    builtins, garage = {'__builtins__': None}, {}
    try: exec(recipe, builtins, garage)
    except: pass

def GFW(func): # Great Firewall of the observable universe and it's infinite timelines
    @functools.wraps(func)
    def federation(*args, **kwargs):
        ingredient = session.get('ingredient', None)
        measurements = session.get('measurements', None)

        recipe = '%s = %s' % (ingredient, measurements)
        if ingredient and measurements and len(recipe) >= 20:
            regex = re.compile('|'.join(map(re.escape, ['[', '(', '_', '.'])))
            matches = regex.findall(recipe)
            
            if matches: 
                return render_template('index.html', blacklisted='Morty you dumbass: ' + ', '.join(set(matches)))
            
            if len(recipe) > 300: 
                return func(*args, **kwargs) # ionic defibulizer can't handle more bytes than that
            
            calc(recipe)
            # return render_template('index.html', calculations=garage[ingredient])
            return func(*args, **kwargs) # rick deterrent

        ingredient = session['ingredient'] = ''.join(random.choice(string.lowercase) for _ in xrange(10))
        measurements = session['measurements'] = ''.join(map(str, [random.randint(1, 69), random.choice(['+', '-', '*']), random.randint(1,69)]))

        calc('%s = %s' % (ingredient, measurements))
        return render_template('index.html', calculations=garage[ingredient])
    return federation

@app.route('/')
@GFW
def index():
    return render_template('index.html')
 
@app.route('/debug')
def debug():
    return Response(open(__file__).read(), mimetype='text/plain')

if __name__ == '__main__':
    app.run('0.0.0.0', port=1337)

La première chose qui nous saute aux yeux c'est cette ligne:

app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'tlci0GhK8n5A18K1GTx6KPwfYjuuftWw')

Elle contient le mot "secret" donc ça doit forcément être important! Ensuite nous pouvons voir qu'il existe deux fonctions ainsi que deux routes:

  • @app.route('/')
  • @app.route("/debug")

Ces routes permettent de définir quelles sont les fonctions qu'il faut exécuter en fonction de la page requêtée par l'utilisateur. Là où ça devient intéressant c'est que la fonction de la route "/" est décorée par la fonction GFW. Or si on regarde cette fonction, on peut voir qu'elle récupère deux valeurs contenues dans le cookie de session via les lignes suivantes:

ingredient = session.get('ingredient', None)
measurements = session.get('measurements', None)

Et voici à quoi ressemble notre cookie de session:

.eJyrVsrMSy9KTclMzStRsqpWUkhSslJKdc_ISY5wK0_OLalMCbS1VarVUcpNTSwuLUrNBaorhiv0DXEs8QUrqAUAlUoYLQ.X0pzdA.Ictuab7r9mp1edsJqQ_Ue0mUTzk

Pas très parlant... Sauf si on lit attentivement la documentation du framework Flask qui nous apprend que les cookies de session sont chiffrés....... à l'aide de la SECRET_KEY que nous avons vu plus tôt! Pour cela j'ai utilisé l'outil suivant: https://github.com/noraj/flask-session-cookie-manager/blob/master/flask_session_cookie_manager3.py développé en  python3. Cet outil nous permettra aussi bien d'encoder et décoder nos cookies de session et ainsi avancer sur ce challenge.