Siunam's Website

My personal website

Home About Blog Writeups Projects E-Portfolio


Table of Contents

  1. Overview
  2. Background
  3. Enumeration
  4. Exploitation
  5. Conclusion



Sign up and see those grades :D! How well did you do this year’s subject? Author: donfran


Home page:

Hmm… Looks like we can sign up a new account:

Burp Suite HTTP history:

After logged in, we can check our assignment grades in /grades route:

In this challenge, we can download a file:

└> file grades_grades_grades.tar.gz 
grades_grades_grades.tar.gz: gzip compressed data, from Unix, original size modulo 2^32 30720
└> tar xf grades_grades_grades.tar.gz 
└> ls -lah grades_grades_grades
total 32K
drwxr-xr-x 3 siunam nam 4.0K Aug 24 10:36 .
drwxr-xr-x 3 siunam nam 4.0K Sep  3 21:04 ..
-rw-r--r-- 1 siunam nam  357 Aug 24 10:36 Dockerfile
-rw-r--r-- 1 siunam nam  175 Aug 22 21:20 Pipfile
-rwxr-xr-x 1 siunam nam   26 Aug 24 10:36 requirements.txt
-rwxr-xr-x 1 siunam nam  116 Aug 24 10:36
-rwxr-xr-x 1 siunam nam   79 Aug 24 10:36
drwxr-xr-x 3 siunam nam 4.0K Aug 24 10:44 src

After digging through the source code, we can view the main logic of the web application in src/ and src/

In src/, we can see there’s a /grades_flag, which will response us with the flag’s content:

@api.route('/grades_flag', methods=('GET',))
def flag():
    return render_template('flag.html', flag="FAKE{real_flag_is_on_the_server}", is_auth=True, is_teacher_role=True)

However, it requires teacher role.

Decorator requires_teacher in src/

SECRET_KEY = secrets.token_hex(32)
def decode_token(token):
        return jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
    except jwt.ExpiredSignatureError:
        return None
def requires_teacher(f):
    def decorated(*args, **kwargs):
        token = request.cookies.get('auth_token')
        if not token:
            return jsonify({'message': 'Token is missing'}), 401
            data = decode_token(token)
            if data is None or data.get("is_teacher") is None:
                return jsonify({'message': 'Invalid token'}), 401
            if data['is_teacher']:
                request.user_data = data
                return jsonify({'message': 'Invalid token'}), 401
        except jwt.DecodeError:
            return jsonify({'message': 'Invalid token'}), 401

        return f(*args, **kwargs)

    return decorated

In here, it’s verifying our JWT from the cookie, and it’s checking the JWT has is_teacher claim.


Hmm… How can we sign arbitrary JWT… We can’t crack the secert key because of random 32 bytes.

After fumbling around, I found that the /signup route is very interesting:

@api.route('/signup', methods=('POST', 'GET'))
def signup():

    # make sure user isn't authenticated
    if is_teacher_role():
        return render_template('public.html', is_auth=True, is_teacher_role=True)
    elif is_authenticated():
        return render_template('public.html', is_auth=True)

    # get form data
    if request.method == 'POST':
        jwt_data = request.form.to_dict()
        jwt_cookie = current_app.auth.create_token(jwt_data)
        if is_teacher_role():
            response = make_response(redirect(url_for('api.index', is_auth=True, is_teacher_role=True)))
            response = make_response(redirect(url_for('api.index', is_auth=True)))
        response.set_cookie('auth_token', jwt_cookie, httponly=True)
        return response

    return render_template('signup.html')

When POST request is being sent, it’ll convert the request data to data type dictionary, and call function create_token() from src/

def create_token(data):
    token = jwt.encode(data, SECRET_KEY, algorithm='HS256')
    return token

This function will sign the JWT with the given POST data.

Ah ha! What if I provide the is_teacher POST parameter? Will it sign the JWT as normal??

Decoded JWT:

Nice!! Our is_teacher payload claim is there!

Let’s get the flag with that JWT!


What we’ve learned:

  1. Sign arbitrary JWT via flawed signing process