JSON Web Token (JWT)

JSON web tokens (JWTs) are a standardized format for sending cryptographically signed JSON data between systems. They can contain any kind of data but are most commonly used to send information (“claims”) about users as part of authentication, session handling, and access control mechanisms. Based on Open Standard, RFC 7519 (RFC Editor).

💡 See labs WebSecurityAcademy (PortSwigger) – JWT.

BChecks available on GitHub.

JWT Format

Encoded in base 64. Decode each value between the dots with base 64.

header.payload.signature

Header (decoded/encoded)

{"alg":"HS256","typ":"JWT"}
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
{"alg":"none","typ":"JWT"}
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0=

Payload (data)

{"username":"guest"}

Signature

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
your-256-bit-secret
)

In web request

GET /api/someapi/ HTTP/1.1
...
Authorization: Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0=.payload.signature
...

JWK (JSON Web Key)

Standardized format for representing keys as a JSON object.

💡 JWK Sets are sometimes exposed publicly via a standard endpoint, such as “/jwks.json” and “/.well-known/jwks.json”.

Example of jwks.json

{"keys":[{"kty":"RSA","e":"AQAB","use":"sig","kid":"119f5de5-4f71-4e18-9a2c-afad2e363dae","alg":"RS256","n":"..."}]}

JWKs to PEM

See lab Expert – JWT authentication bypass via algorithm confusion.

  • Install/load the JWT Editor extension in Burp Suite.
  • Click on the JWT Editor Keys tab and click New RSA Key.
  • Select JWK as the Key Format.
  • Paste the content of the public key and click OK.
  • Right-click on the key and select Copy Public Key as PEM.

Testing JWT

💡 PortSwigger recommends installing the JWT Editor extension, which is available from the BApp Store (PRO version). 

Unverified signature – Nothing is validated

Scenario: The server does not verify the signature of any JWTs that it receives. There is no verification at all.

Using the Inspector

  • Send a request to the Repeater module.
  • Select the payload part of the JWT (header.payload.signature) and open the Inspector.
  • In the Inspector, under Decoded from Base64, make modifications in the payload (like changing the username) and click Apply changes.
  • Send the request.

Unverified signature – Algorithm “none”

Scenario: The server is insecurely configured to accept unsigned JWTs. The algorithm “none” is accepted.

Using the Inspector

  • Send a request to the Repeater module.
  • Select the header part of the JWT (header.payload.signature) and open the Inspector.
    • Under Decoded from Base64, modify the “alg” parameter to “none” and click Apply changes.
  • Select the payload part of the JWT (header.payload.signature) and open the Inspector.
    • Under Decoded from Base64, make modifications in the payload (like changing the username) and click Apply changes.
  • Remove the signature from the JWT, but leave the last dot, like “header.payload.”.
  • Send the request.

Using the JWT Editor extension

  • Send a request to the Repeater module.
  • In the JSON Web Token tab of the request, make modifications if needed (like changing the username).
  • Click on Attack and choose “none” Signing Algorithm.
  • Send the request.

Weak signing key – Bruteforce the key

Scenario: The secret key used to both sign and verify tokens is weak and can be bruteforced.

❗ Some frameworks like ASP.NET Boilerplate (ABP) use weak secrets. See .NET Cheat Sheet (C#, ASP.NET).

Create a file jwt_token.txt containing the JWT:

echo 'header.payload.signature' > jwt_token.txt

Use Hashcat with a wordlist of common secrets to bruteforce the signature.

💡 The HS algorithm (HS256/HS512) uses a secret key to sign and verify messages. The signature is created by taking both the header and the payload, use a secret key to create a signature by using the algorithm specified in the JWT header. Did not work for RS256 (public/private keys).

wget https://raw.githubusercontent.com/wallarm/jwt-secrets/master/jwt.secrets.list
hashcat -a0 -m 16500 jwt_token.txt jwt.secrets.list --force
hashcat -m 16500 jwt_token.txt --show

Also try a mask attack from 1 to 8 characters:

hashcat -a3 -m 16500 --increment --increment-min=1 --increment-max=8 jwt_token.txt ?a?a?a?a?a?a?a?a --force

If found, the secret will be at the end. Use https://jwt.io to modify and sign the JWT:

  • Make modifications in the payload (like changing the username).
  • Under Verify signature, enter the secret key previously found.
  • Copy the JWT token generated.
  • In the Repeater tab, update the JWT to this new value.

JWT header parameter injections

See JWT header parameter injections (PortSwigger). The only mandatory parameter is “alg”.

Injecting self-signed JWTs via the jwk parameter

jwk (JSON Web Key): optional header parameter, provides an embedded JSON object representing the key which servers can use to embed their public key directly within the token itself in JWK format.

  • Install/load the JWT Editor extension in Burp Suite.
  • Click on the JWT Editor Keys tab and create a new RSA key if needed.
  • In the Repeater tab, click on the request that you want to edit (e.g. “GET /admin”).
  • Click on the JSON Web Token tab in the request.
    • In the Payload section, replace the user (“normal-user”) by another user (“administrator”). You can skip this step to check how the application reacts using the embedded jwk.
    • Click on Attack, and select Embedded JWK. Select the RSA key created. Click OK.
  • Go back to the Raw tab and send the request.

Injecting self-signed JWTs via the jku parameter

Some servers let you use the jku (JWK Set URL) header parameter to reference a JWK Set (array of JWKs) containing the key. When verifying the signature, the server fetches the relevant key from this URL. JWK Sets like this are sometimes exposed publicly via a standard endpoint, such as /.well-known/jwks.json.

  • Install/load the JWT Editor extension in Burp Suite.
  • Click on the JWT Editor Keys tab and create a new RSA key (if needed).
  • Right-click on the key and select Copy Public Key as JWK. Keep note of this information.

Start a web server and host this payload in the page’s body (use Exploit Server from lab JWT authentication bypass via jku header injection). Paste key the previously copied. Name file “jwks.json”.

{
    "keys": [
<COPY YOUR PUBLIC KEY AS JWK HERE>
    ]
}
{
    "keys": [
{
    "kty": "RSA",
    "e": "AQAB",
    "kid": "42ce64d2-d0af-4ab2-8c42-68e5e0707ada",
    "n": "..."
}
    ]
}
  • In the Repeater tab, click on the request that you want to edit (e.g. “GET /admin”).
  • Click on the JSON Web Token tab in the request.
    • In the Payload section, replace the user (“normal-user”) by another user (“administrator”). You can skip this step to check how the application reacts using the jku.
    • In the Header section:
      • Replace the kid value by the kid from your JWT.
      • Add a new jku parameter and set its value to the URL of your JWK Set on your web server.
    • Click on Sign, and select the RSA key corresponding with the kid used. Select Don’t modify header. Click OK.
  • Go back to the Raw tab and send the request.

Header should look like:

{
    "kid": "42ce64d2-d0af-4ab2-8c42-68e5e0707ada",
    "alg": "RS256",
    "jku": "https://<YOUR WEB SERVER>/<JWK Set file>"
}

Injecting self-signed JWTs via the kid parameter

If the kid parameter is vulnerable to Directory Traversal and the server supports JWTs signed with a symmetric algorithm, the attacker can point the kid to a static file and sign the JWT using a secret that matches the content of this file. Use the /dev/null file (empty file) and sign using an empty string (encoded in base 64).

💡 The easiest way is to use jwt.io.

  • Copy the JWT in jwt.io.
  • In the Header section, replace the “kid” value with “../../../../../dev/null”.
  • In the Payload section, modify the user or any other modifications.
  • In the Verify signature section, remove the string in the secret textbox.
  • Copy the generated JWT and use it in Burp Suite.

❗ The JWT Editor extension won’t allow you to sign tokens using an empty string, so use this workaround.

  • Install/load the JWT Editor extension in Burp Suite.
  • Click on the JWT Editor Keys tab and click New Symmetric Key.
  • Click on Generate (do not change anything else).
  • Replace the “k” parameter with the null byte (encoded in base 64): “AA==” and click OK.

Key should look like:

{
    "kty": "oct",
    "kid": "051ce12b-010c-418a-82da-e150934df484",
    "k": "AA=="
}
  • In the Repeater tab, click on the request that you want to edit (e.g. “GET /admin”).
  • Click on the JSON Web Token tab in the request.
    • In the Payload section, replace the user (“normal-user”) by another user (“administrator”). You can skip this step to check how the application reacts with the changes.
    • In the Header section, replace the kid value by “../../../../../dev/null”.
    • Click on Sign, and select the key that you just created. Select Don’t modify header. Click OK.
  • Go back to the Raw tab and send the request.

Algorithm confusion

Scenario: The server uses an RSA key pair to sign and verify tokens, and this mechanism is vulnerable to algorithm confusion attacks due to implementation flaws. Requires to find the public key (jwks.json) or derive the public key from 2 JWTs.

Option 1: Obtain the server’s public key

Try finding the public key at standard endpoints like “/jwks.json” and “/.well-known/jwks.json”.

{"keys":[{"kty":"RSA","e":"AQAB","use":"sig","kid":"119f5de5-4f71-4e18-9a2c-afad2e363dae","alg":"RS256","n":"..."}]}

Option 2: Derive the public key from JWTs

  • Log in the application and obtain one token.
  • Log out and log in again to obtain a second token.
  • Derive the public key with jwt_forgery.py. Use the docker container from PortSwigger.
JWT1=eyJ...
JWT2=eyJ...
# Fix for "docker: Error response from daemon: cgroups: cgroup mountpoint does not exist: unknown."
sudo mkdir /sys/fs/cgroup/systemd
sudo mount -t cgroup -o none,name=systemd cgroup /sys/fs/cgroup/systemd
sudo docker run --rm -it portswigger/sig2n $JWT1 $JWT2

The output contains a Base64-encoded PEM key in both X.509 and PKCS1 format, and a forged JWT signed using each of these keys. To identify the correct key, use Burp Repeater to send a request containing each of the forged JWTs. Only one of these will be accepted by the server. You can then use the matching key to construct an algorithm confusion attack.

Generate a malicious signing key

If you used Option 1 (from jwks.json file):

  • Install/load the JWT Editor extension in Burp Suite.
  • Click on the JWT Editor Keys tab and click New RSA Key.
  • Select JWK as the Key Format.
  • Paste the content of the public key and click OK ({“kty”:”RSA” …, “n”:”…”}).
  • Right-click on the key and select Copy Public Key as PEM.
  • Encode the key (PEM format) in base 64 using the Decoder tab. Be careful to keep the carriage return from the PEM when encoding.

For both Option 1 and Option 2:

  • Click on the JWT Editor Keys tab and click New Symmetric Key.
  • Leave the Key Size and Key empty. Click on Generate to generate a new key in JWK format.
  • Replace the generated value for the “k” property with the base 64 PEM that you just created (Option 1) or Base64 encoded x509 key (Option 2) and click OK.
{
    "kty": "...",
    "kid": "...",
    "k": "<BASE 64 PEM>"
}

Modify and sign the token

  • In the Repeater tab, click on the request that you want to edit (e.g. “GET /admin”).
  • Click on the JSON Web Token tab in the request.
    • In the Payload section, replace the user (“normal-user”) by another user (“administrator”). You can skip this step to check how the application reacts with the changes.
    • In the Header section, replace the alg value by “HS256”.
    • Click on Sign, and select the symmetric key that you just created. Select Don’t modify header. Click OK. The modified token is now signed using the server’s public key as the secret key.
  • Go back to the Raw tab and send the request.