Breaking JSON Web Tokens


JSON Web Tokens (JWT) are commonly used to implement authentication and authorization on websites and APIs. While there are numerous cases for why you really should not use JWT in your applications, it is very common to see them all around the internet as API and session tokens.

We are going to take a look at common vulnerabilities in JWT implementations, but first a quick overview.

What is a JWT?

A JSON Web Token is a long string consisting of three base64url encoded parts: the header, the payload and the signature.

The header, called the JOSE header (JSON Object Signing and Encryption) specifies the algorithm used to sign and encrypt the token. Most JWTs are just signed and not encrypted. The most common algorithms are:

  • HS256 (HMAC + SHA256)
  • RS256 (RSASSA-PKCS1-v1_5 + SHA256)
  • ES256 (ECDSA + P-256 + SHA256)

Other supported algorithms are specified in RFC7518 section 3.

A typical JWT header looks like this:

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

The payload contains a set of claims in JavaScript Object Notation. There are some claims that are recommended, such as iss (issuer), exp (expiration time), sub (subject), aud (audience), and others, but they are optional. For example, a JWT that allows the user “jane” to authenticate to the API over at https://secure.website until the year 2030 might look something like this:

{
  "iss": "https://secure.website",
  "sub": "jane",
  "name": "Jane Doe",
  "iat": 1516239022,
  "exp": 1893456000,
  "aud": "https://api.secure.website"
}

More info about JWT claims can be found in the specification (RFC7519 section 4.1)

The last part of the token is the signature. For HS256, this is calculated as follows:

HMAC-SHA256( base64urlEncoding(header) + '.' +  base64urlEncoding(payload), secret)

In conclusion, a typical JWT in web request headers looks like this:

GET /orders?id=123 HTTP/1.1
Host: fancy.shop
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3NlY3VyZS53ZWJzaXRlIiwic3ViIjoiamFuZSIsIm5hbWUiOiJKYW5lIERvZSIsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxODkzNDU2MDAwLCJhdWQiOiJodHRwczovL2FwaS5zZWN1cmUud2Vic2l0ZSJ9.-otVqVkjXahy49TO-taezh2q3MUAknhP16uC0MPRCVI

Decoding a token

In order to figure out the contents of an existing token, the steps would have to be reversed. Splitting the JWT by periods and separately base64 decoding the parts will return the JSON contents.

There are multiple ways to decode a base64 string, such as:

Keep in mind that the JWT standard uses the base64url encoding instead of base64 - the differences are minute, but sometimes base64url encoded strings do not decode properly with a decoder designed for base64 only due to some modifications made to the characters used in the encoded string to make it safe to be included in URLs without messing things up. You can read more about this from Wikipedia.

Creating your own tokens

For creating a JWT manually we would need to do the following:

  • Create the JSON header and convert it to a UTF8 string
  • Create the JSON payload and convert it to a UTF8 string
  • Encode each with base64url encoding and join them with a period (the . character)
  • Sign this string with the appropriate secret key
  • Encode the signature with base64url encoding
  • Append the signature to the string with a prepended period

For example if you wanted to use the HS256 algorithm:

token = HS256(base64urlencode(header) + "." + base64urlencode(payload), secret_key)

If that sounds tedious, then it’s probably because it is. For that reason, there are tools both online and offline to make this process easier. Some tools you can try are:

JSON Web Keys

If the token is signed by another party, there needs to be a way to verify that the token issued to you is valid. In order to verify a token, you need access to the public key. For that reason, the JSON Web Key specification is used.

A collection of JSON Web Keys is referred to as a JSON Web Keys Set (JWKS for short), which is a set of keys containing the public keys that should be used to verify any JSON Web Token issued by the authorization server. In essence, JWKS is a JSON object with a “keys” member which is an array of JWKs.

Usually this set of keys is a publicly available JSON file hosted on the authorization server. Some example public endpoints of Microsoft, Google and Amazon include:

  • https://login.microsoftonline.com/common/discovery/v2.0/keys
  • https://www.googleapis.com/oauth2/v3/certs
  • https://cognito-idp.{region}.amazonaws.com/{userPoolId}/.well-known/jwks.json

In order to get the key to a format we can readily use to validate tokens it should be converted from JWK to PEM format. An easy solution to this is to use an online tool such as keytool.online.

An example JWK and its corresponding PEM (PKCS#8) format is shown below:

{
  "kty": "RSA",
  "e": "AQAB",
  "use": "sig",
  "alg": "RS256",
  "n": "kClKaxbcH_5Qr2KACHSHJ-BXnZQWLrhXRSIiAVR9FYFeDbRNaIq3YLSZFKcxH8FOtUWfnX-jzacLlk9caa2FN_PNLGe93GqLWBJ5zcjMPElZ1m44biH9g4alOCl0V1iVlAqQdoIbxXNq418qmcCEhycNiTRZX16jNwdgPCOAuF8"
}

is equivalent to

-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCQKUprFtwf/lCvYoAIdIcn4Fed
lBYuuFdFIiIBVH0VgV4NtE1oirdgtJkUpzEfwU61RZ+df6PNpwuWT1xprYU3880s
Z73caotYEnnNyMw8SVnWbjhuIf2DhqU4KXRXWJWUCpB2ghvFc2rjXyqZwISHJw2J
NFlfXqM3B2A8I4C4XwIDAQAB
-----END PUBLIC KEY-----

Alternative to JWK

If the public key is not exposed via JWKS, there is a possibility that the TLS certificate of the website/API is reused to sign and verify JSON Web Tokens. In order to get the public key, openssl can be used as follows:

openssl s_client -servername blog.rangeforce.com -connect rangeforce.com:443 | openssl x509 -pubkey -noout

This will output the public key of the TLS certificate used by blog.rangeforce.com in PEM format.

The “none” algorithm

JWT specification allows for a “none” algorithm. Tokens using the “none” algorithm are considered as already verified by some implementations, therefore any signature will be valid, which means the last part ofthe JWT can just be left blank. To create such a token, set the algorithm in the decoded header to “none” and use an empty string as the signature. An example of this vulnerability is CVE-2018-1000531.

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

Example of a token with the “none” algorithm and an empty signature (note the trailing dot):

eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJpc3MiOiJodHRwczovL3NlY3VyZS53ZWJzaXRlIiwic3ViIjoiamFuZSIsIm5hbWUiOiJKYW5lIERvZSIsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxODkzNDU2MDAwLCJhdWQiOiJodHRwczovL2FwaS5zZWN1cmUud2Vic2l0ZSJ9.

The mitigation for this vulnerability is to disallow tokens with the “none” algorithm when a key is provided for verification and the token is expected to be signed.

RS256 vs HS256

Two most common algorithms used to sign JWTs are the asymmetrical RS256 algorithm and the symmetrical HS256.

  • HS256 uses a single secret to both create and verify the signature
  • RS256 uses a public/private key pair - private key for signing the token and the public key for verification.

Common code for verifying a JWT looks like this:

jwt.verify(string token, string key)

Since the algorithm to use is stored inside the JWT header, the key could be either the HMAC secret or the RSA public key - this can cause confusion.

If you change the algorithm in the JWT header from RS256 to HS256, the backend code uses the public key as the secret key and then uses the HS256 algorithm to verify the signature.

If the public key is available for use by the attacker (via JWKS, the TLS certificate or by some other means), it can be used in place of the HMAC secret to fabricate valid tokens!

Please note that online JWT generators usually strip all newlines from the key. Keep in mind that the public key must be in the exact same format as it is stored in the backend, so it must match the PEM format and also include newlines (even the trailing one, if it is included in the original PEM file). Online tools like jwt.io will mess with newlines in the secret, so be sure to base64encode the whole key in PEM format and use the secret base64 encoded option.

Cracking the secret

The RFC7518 standard states that “A key of the same size as the hash output (for instance, 256 bits for “HS256”) or larger MUST be used with this algorithm.”

It’s almost impossible to crack a 256-bit key. However, sometimes developers take shortcuts and do not generate secure keys for signing and verifying their tokens. For example, the jwt.io uses “your-256-bit-secret” as the default HS256 secret, and many code samples use the string “secret”.

Testing the different secret strings by hand can be cumbersome and there are tools that you can use to simplify this.

c-jwt-cracker

This is a simple tool written in C that can be used to crack the JWT secret.

https://github.com/brendan-rius/c-jwt-cracker

Sample usage:

$ ./jwtcrack eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.cAOIAifu3fykvhkHpbuhbvtH807-Z2rI1FS3vX1XMjE

hashcat

Hashcat also supports cracking JWT secrets. If the token is stored in a text file called jwt.txt, the following command can be used to guess the secret string.

$ ./hashcat -m 16500 jwt.txt -a 3 -w 3 ?a?a?a?a?a?a

JWT_Tool

JWT_Tool is an all-around tool suitable for pentesters and developers who want to test how their application behaves with forged tokens.

Available at https://github.com/ticarpi/jwt_tool, its functionality includes:

  • Checking the validity of a token
  • Testing for the RS/HS256 public key mismatch vulnerability
  • Testing for the alg=None signature-bypass vulnerability
  • Testing the validity of a secret/key/key file
  • Identifying weak keys via a High-speed Dictionary Attack
  • Forging new token header and payload values and creating a new signature with the key or via another attack method

When testing jwt_tool, I initially found that the alg=”none” vulnerability capitalizes the “None” string and may require manual tweaking with certain JWT libraries. This has been fixed in JWT_tool since 1.2.1, so make sure to use the latest version.

$ python jwt_tool.py eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3NlY3VyZS53ZWJzaXRlIiwic3ViIjoiamFuZSIsIm5hbWUiOiJKYW5lIERvZSIsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxODkzNDU2MDAwLCJhdWQiOiJodHRwczovL2FwaS5zZWN1cmUud2Vic2l0ZSJ9.-otVqVkjXahy49TO-taezh2q3MUAknhP16uC0MPRCVI

,----.,----.,----.,----.,----.,----.,----.,----.,----.,----.
----''----''----''----''----''----''----''----''----''----'
     ,--.,--.   ,--.,--------.,--------.             ,--.
     |  ||  |   |  |'--.  .--''--.  .--',---.  ,---. |  |
,--. |  ||  |.'.|  |   |  |      |  |  | .-. || .-. ||  |
|  '-'  /|   ,'.   |   |  |,----.|  |  ' '-' '' '-' '|  |
 `-----' '--'   '--'   `--''----'`--'   `---'  `---' `--'
,----.,----.,----.,----.,----.,----.,----.,----.,----.,----.
'----''----''----''----''----''----''----''----''----''----'

Token header values:
[+] alg = HS256
[+] typ = JWT

Token payload values:
[+] iss = https://secure.website
[+] sub = jane
[+] name = Jane Doe
[+] iat = 1516239022
[+] exp = 1893456000
[+] aud = https://api.secure.website

######################################################
# Options:                                           #
# 1: Check CVE-2015-2951 - alg=None vulnerability    #
# 2: Check for Public Key bypass in RSA mode         #
# 3: Check signature against a key                   #
# 4: Check signature against a key file ("kid")      #
# 5: Crack signature with supplied dictionary file   #
# 6: Tamper with payload data (key required to sign) #
# 0: Quit                                            #
######################################################

Please make a selection (1-6)
>

Conclusion

If you are considering using JSON Web Tokens in your application, make sure you are using a secure library, verify that only the correct algorithms are used and that your application rejects forged tokens.

Kert Ojasoo