Sitemap

JWT Security — TryHackMe

12 min readJul 25, 2026

--

Press enter or click to view image in full size

Here I will show you how to find all flags in this room. So that’s your clear task: 1, 2, 3 first. Just read this task detail and answer it.

After you complete this task, then jump to the practical task.

Let’s go:

Task -04

Q-1. What is the flag for example 1?

Practical Example 1

Let’s take a look at a practical example. Let’s authenticate to our API using the following cURL request:

curl -H 'Content-Type: application/json' -X POST -d '{ "username" : "user", "password" : "password1" }' http://10.48.144.1/api/v1.0/example1

This will provide you with a JWT token. Once recovered, decode the body of the JWT to uncover sensitive information. You can decode the body manually or use a website such as JWT.io(opens in new tab) for this process.

The Development Mistake

In the example, sensitive information was added to the claim, as shown below:

payload = {
"username" : username,
"password" : password,
"admin" : 0,
"flag" : "[redacted]"
}
access_token = jwt.encode(payload, self.secret, algorithm="HS256")

The Fix

Values such as the password or flag should not be added as claims as the JWT will be sent client-side. Instead, these values should be securely stored server-side in the backend. When required, the username can be read from a verified JWT and used to lookup these values, as shown in the example below:

payload = jwt.decode(token, self.secret, algorithms="HS256")
username = payload['username']
flag = self.db_lookup(username, "flag")

After reading this properly, jump to your machine or in thm vm and find out the target flag:

  1. Collect example1 user token.
Press enter or click to view image in full size

2. Copy the token and go to jwt.io past this token in the token section, and then you see the example1 flag.

Press enter or click to view image in full size

Task-05

Q-1. What is the flag for example 2?

Practical Example 2

Let’s authenticate to the API:

curl -H 'Content-Type: application/json' -X POST -d '{ "username" : "user", "password" : "password2" }' http://MACHINE_IP/api/v1.0/example2

Once authenticated, let’s verify our user:

curl -H 'Authorization: Bearer [JWT Token]' http://MACHINE_IP/api/v1.0/example2?username=user

However, let’s try to verify our user without the signature, remove the third part of the JWT (leaving only the dot) and make the request again. You will see that the verification still works! This means that the signature is not being verified. Modify the admin claim in the payload to be 1 and try to verify as the admin user to retrieve your flag.

The Development Mistake

In the example, the signature is not being verified, as shown below:

payload = jwt.decode(token, options={'verify_signature': False})

While it is rare to see this on normal APIs, it often happens on server-to-server APIs. In cases where a threat actor has direct access to the backend server, JWTs can be forged.

The Fix

The JWT should always be verified or additional authentication factors, such as certificates, should be used for server-to-server communication. The JWT can be verified by providing the secret (or public key), as shown in the example below:

payload = jwt.decode(token, self.secret, algorithms="HS256")

Downgrading to None

Another common issue is a signature algorithm downgrade. JWTs support the None signing algorithm, which effectively means that no signature is used with the JWT. 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.

After reading this properly, jump to your machine or in thm vm and find out the target flag:

  1. Collect token
Press enter or click to view image in full size

2. Go to jwt.io and paste it

Press enter or click to view image in full size

3. Now change username, admin role and generate a new token. Copy this new token.

Press enter or click to view image in full size

4. Come in attack machin and try with new token and in output you see the example2 flag.

Press enter or click to view image in full size

Q-2. What is the flag for example 3?

Practical Example 3

Authenticate to the API to receive your JWT and then verify your user. To perform this attack, you will need to manually alter the the alg claim in the header to be None. You can use CyberChef(opens in new tab) for this making use of the URL-Encoded Base64 option. Submit the JWT again to verify that it is still accepted, even if the signature is no longer valid, as changes have been made. You can then alter the admin claim to recover the flag.

The Development Mistake

While this may seem like the same issue as before, from a development perspective, it is slightly more complex. Sometimes, developers want to ensure their implementation accepts several JWT signature verification algorithms. The implementation would then usually read the header of the JWT and parse found alg into the signature verification component, as shown below:

header = jwt.get_unverified_header(token)
signature_algorithm = header['alg']payload = jwt.decode(token, self.secret, algorithms=signature_algorithm)

However, when the threat actor specified None as the algorithm, signature verification is bypassed. Pyjwt(opens in new tab), the JWT library used in this room, has implemented security coding to prevent this issue. If a secret is specified when the None algorithm is selected, an exception is raised.

The Fix

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"])
username = payload['username']
flag = self.db_lookup(username, "flag")

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.

After reading this properly, jump to your machine or in thm vm and find out the target flag:

  1. Collect a new token and go to CyberChef and change the first part of the token into none algorithm.
Press enter or click to view image in full size

2. Now replace this in the token first part and enter. You see the example3 flag in output.

Press enter or click to view image in full size

Q-3. What is the flag for example 4?

Practical Example 4

For this example, a weak secret was used to generate the JWT. Once you receive a JWT, you have several options to crack the secret. For our example, we will talk about using Hashcat(opens in new tab) to crack the JWT’s secret. You could also use other solutions such as John(opens in new tab) as well. You can use the following steps to crack the secret:

  1. Save the JWT to a text file called jwt.txt.
  2. 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.
  3. Use Hashcat to crack the secret using hashcat -m 16500 -a 0 jwt.txt jwt.secrets.list

Once you know what the secret is, you can forge a new admin token to recover the flag!

Get Fuad Hasan’s stories in your inbox

Join Medium for free to get updates from this writer.

The Development Mistake

The issue occurs when a weak JWT secret is used. This can often occur when developers are in a hurry or copy code from examples.

The Fix

A secure secret value should be selected. As this value will be used in software and not by humans, a long, random string should be used for the secret.

Signature Algorithm Confusion

The last common issue with signature validation is when an algorithm confusion attack can be performed. 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.

After reading this properly, jump to your machine or in thm vm and find out the target flag:

  1. Collect token and write it in a txt file.
Press enter or click to view image in full size
Press enter or click to view image in full size

2. After creating the file with the token, then download jwt.secrets.list file and try hashcat to find out the secret key.

Press enter or click to view image in full size

3. Now go to xjwt.io and create a new token for the admin role using the secret key.

Press enter or click to view image in full size

4. Now try with the new token and see the output example4 flag.

Press enter or click to view image in full size

Q-4. What is the flag for example 5?

Practical Example 5

This is similar to example 3. Except this time, the None algorithm is not allowed. However, once you authenticate to the example, you will also receive 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)

Note: We recommend that you use the AttackBox for this practical example since Pyjwt is already installed for you. 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. If you are using your own VM, you may have to install Pyjwt (pip3 install pyjwt) to use this script. You will also need to tamper with the Pyjwt library's algorithm.py file on line 258 to remove the is_ssh_key condition as a patch for this vulnerability was released. Keep in mind that this placement can vary per VM and installation. An easier method if you are not comfortable with library code edits is to make use of jwt.io(opens in new tab). Once you verify it works, you can alter the claims to make yourself an admin and recover the flag.

The Development Mistake

The mistake in this example is similar to that of example 3 but a bit more complex. While the None algorithm is disallowed, the key issue stems from both symmetric and asymmetric signature algorithms being allowed, as shown in the example below:

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.

The Fix

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")

After reading this properly, jump to your machine or in thm vm and find out the target flag:

  1. First, collect the token and public key.
Press enter or click to view image in full size

2. Now go to jwt.io and create a new token for example5 flag. Change alg, username, admin role, and also give the public key.

Press enter or click to view image in full size

3. Then come to the attack machine and try with this token and find out the example5 flag.

Press enter or click to view image in full size

Task-06

Q-1. What is the flag for example 6?

Practical Example 6

In this example, the JWT implementation did not specify an exp value, meaning tokens are permanently persistent. Use the token below to recover your flag:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InVzZXIiLCJhZG1pbiI6MX0.ko7EQiATQQzrQPwRO8ZTY37pQWGLPZWEvdWH0tVDNPU

The Development Mistake

As mentioned above, the JWT does not have an exp value, meaning it will be persistent. In the event that an exp claim isn't present, most JWT libraries would accept the token as valid if the signature is verified.

The Fix

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")

After reading this properly, jump to your machine or in thm vm and find out the target flag:

  1. Just use the given token with username=admin, and in the output you see the example6 flag.
Press enter or click to view image in full size

Task-07

Q-1. What is the flag for example 7?

Practical Example 7

For this last practical example, there are two API endpoints namely example7_appA and example7_appB. You can use the same GET request you made in the previous examples to recover the flag, but you will need to point it to these endpoints. Furthermore, for authentication, you now also have to include the "application" : "appX" data value in the login request made to example7. Use the following steps to perform the example:

  1. Authenticate to example7 using the following data segment: '{ "username" : "user", "password" : "password7", "application" : "appA"}'. You will notice that an audience claim is added, but that you are not an admin.
  2. Use this token in both the admin and user requests you make to example7_appA and example7_appB. You will notice that while appA accepts the token, you are not an admin, and appB does not accept the token as the audience is incorrect.
  3. Authenticate to example7 using the following data segment: '{ "username" : "user", "password" : "password7", "application" : "appB"}'. You will notice that an audience claim is added again and you are an admin this time.
  4. Use this token again to verify yourself on both applications and see what happens.

You can use this to now recover your flag.

The Development Mistake

The key issue is that the audience claim is not being verified on appA. This can be either because audience claim verification has been turned off or the audience scope has been set too wide.

The Fix

The audience claim should be verified when the token is decoded. This can be done as shown in the example below:

payload = jwt.decode(token, self.secret, audience=["appA"], algorithms="HS256")

After reading this properly, jump to your machine or in thm vm and find out the target flag:

  1. Find out the token for appA, aaB with the given authentication values.
Press enter or click to view image in full size

2. Try to find out the flag thus tokens. For the flag, use token appB and in the URL change _appB into _appA, then you see the example7 flag in the output.

Press enter or click to view image in full size

After you find all flags in this room, you will now have proper knowledge about JWT. Now you can explore this in you real life scenario.

Thanks to all. See you soon!

--

--

Differences give birth to new things

See more from me