Facebook CTF 2019 Writeup: events – Template Injection and Cookie Forgery

Problem Description

I heard cookies and string formatting are safe in 2019?
http://challenges.fbctf.com:8083
(This problem does not require any brute force or scanning. 
We will ban your team if we detect brute force or scanning).

Application Overview

From the problem description it looks like it's going to be about Cookie Forgery and Server Side Template Injection (SSTI).
On opening the page we are greeted with a login/register form, similar the other web challenges. After logging in, we are greeted with this page:

Homepage after log in

On submission, the 3 form fields are sent to the server. Form submission data

The homepage then displays the event, according to the property we chose. Submission display

Template Injection

After inserting some basic SSTI payloads to the name and address field with no success. I decided to mess around with the event_important field because it looked suspicious enough. And sure enough, after setting event_important to "id" something happened. Displaying the event's id

Because the problem description mentions about string formatting, this is most likely an SSTI vulnerability just to be sure I also tried to set the event_important to "__dict__" to check if we are actually accessing an object's property. It turned out, not only did this verify that it is actually a template injection but also how it is being done. Displaying the event's __dic__ Judging from the fmt property which uses argument numbers and single curly braces, the server most likely uses Python's built-in format method to create the template. There is also the show property which is also a possible template string, so I used the payload "id}{0.id" and confirmed that the property that's actually used is fmt.

Exploitation

There several ways we can further exploit this vulnerability:

We have references to SQL Alchemy objects and classes, so maybe it's somehow possible access the database that way. Unfortunately, Python's format method can only do property accesses and not method calls so it's probably going to be really hard if not impossible to query the database. (I tried lol)

Another way worth trying is to access the global objects and modules in the application and hope that there's something interesting inside them. One way to access globals is through the __globals__ property of a function. One function we can access is the __init__ method of the event object. And voila! there's lots of interesting stuff for us to traverse. One of particular interest is the Flask app instance.

Globals of __init__ method

We can further inspect the Flask app instance by accessing its __dict__. You might notice that __globals__ is a dictionary. We cannot use the dot property access syntax to access a dictionary's value in a Python format string, instead we have to use a weird new syntax dictionary[key_string] (notice that there are no quotes). Sending __init__.__globals__[app].__dict__ allows us to see many of the app's settings and properties.

__dict__ of app

There are definitely many properties and methods that are interesting, such as the view methods and login manager as well as the SECRET_KEY which is always very interesting.

At this point I didn't realize that by default Flask uses signed cookies, which means the server signs the session data and sends it back to the client. Signed cookies sessions are meant to reduce server load by off-loading session data to the client while keeping it secure by signing it with a secret. Unfortunately this makes it very easy for an attacker with a secret key to forge session data by creating the session data and then signing it using the secret key. This why you should as keep your secret key, secret.

Cookie Forgery

After learning that Flask uses signed cookies by default (thanks to Flask's awesome documentation) I became certain that the solution was to craft a signed cookie using the retrieved secret_key. I ran a Flask app to forge signed cookies.

from flask import Flask
from flask.sessions import SecureCookieSessionInterface

app = Flask(__name__)
app.secret_key = b'fb+wwn!n1yo+9c(9s6!_3o#nqm&&_ej$tez)$_ik36n8d7o6mr#y'

session_serializer = SecureCookieSessionInterface().get_signing_serializer(app)

@app.route('/')
def index():
    print(session_serializer.dumps("admin"))
    return "lol"

There were two cookies events_sesh_cookies and user. I tried forging the events_sesh_cookies first, setting the id in the session data to 1 (most likely to be the admin). It successfully authenticated me as another user but didn't grant me admin authorization. So I looked to the other cookie, user. Decoding it reveals that it is actually storing the user's username. Then I thought that changing the user cookie data to "admin" will probably authorize me as an admin. After forging the user cookie, accessing /flag gives the flag:

/flag as admin

Flag: fb{e@t_aLL_th0s3_c0oKie5}