siunam's Website

My personal website

Home Writeups Blog Projects About E-Portfolio

grades_grades_grades

Table of Contents

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

Overview

Background

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

https://web-grades-grades-grades-c4627b227382.2023.ductf.dev

Enumeration

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:

┌[siunam♥Mercury]-(~/ctf/DownUnderCTF-2023/web/grades_grades_grades)-[2023.09.03|21:04:04(HKT)]
└> file grades_grades_grades.tar.gz 
grades_grades_grades.tar.gz: gzip compressed data, from Unix, original size modulo 2^32 30720
┌[siunam♥Mercury]-(~/ctf/DownUnderCTF-2023/web/grades_grades_grades)-[2023.09.03|21:04:05(HKT)]
└> tar xf grades_grades_grades.tar.gz 
┌[siunam♥Mercury]-(~/ctf/DownUnderCTF-2023/web/grades_grades_grades)-[2023.09.03|21:04:07(HKT)]
└> 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 run.py
-rwxr-xr-x 1 siunam nam   79 Aug 24 10:36 run.sh
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/auth.py and src/routes.py.

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

@api.route('/grades_flag', methods=('GET',))
@requires_teacher
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/auth.py:

SECRET_KEY = secrets.token_hex(32)
[...]
def decode_token(token):
    try:
        return jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
    except jwt.ExpiredSignatureError:
        return None
[...]
def requires_teacher(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        token = request.cookies.get('auth_token')
        if not token:
            return jsonify({'message': 'Token is missing'}), 401
        try:
            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
            else:
                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.

Exploitation

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)))
        else:
            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/auth.py:

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!

Conclusion

What we’ve learned:

  1. Sign arbitrary JWT via flawed signing process