The Lab: JSON Web Tokens

2022/04/15

Tags: jwt

A deep dive into JSON Web Tokens: what they are, how they’re structured, and more specifically, common misconfigurations and exploits.

A JWT

JWTs are a core part of the OpenID Connect standard: a standard which extends the identity layer on top of OAuth 2.

Whilst the OAuth 2 specification does not stipulate a format for access tokens, the industry has widely adopted the use of JWTs.

JWTs are considered stateless. The authorization server does not need to maintain a list of valid JWTs as JWTs are signed using a cryptographic signature which can be checked each time a JWT is used.

From section 3 of RFC 7519:

A JWT is represented as a sequence of URL-safe parts separated by period ('.') characters. Each part contains a base64url-encoded value.

The line breaks in the example below are for display purposes only:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Decoding the JWT reveals the contents:

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

JWTs can be decoded by the JWT debugger.

Signing and cracking JWTs

From the RFC:

The header describes the cryptographic operations applied to the JWT and optionally, additional properties of the JWT.

Encrypted JWTs contain a header, known as the JOSE (JSON Object Signing and Encryption) header. This header defines two attributes: alg and typ.

An example header can be seen below:

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

It’s worth noting that a signed JWT is known as a “JWS”. This can be a bit confusing because “JWT” is commonly used in language to descibe what is actually a “JWS”.

The alg field

alg is the algorithm that was used to sign or encrypt the JWT and can have one of may values. The table below is taken from section 3 of RFC 7518.

“alg” Param Digital Signature or MAC Algorithm Implementation 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 Optional

The use of “+” in the Implementation Requirements column indicates that the requirement strength is likely to be increased in a future version of the specification.

The most common algorithms used are: HS256, RS256, ES256.

“alg”: “HS256”

The HMAC SHA-256 MAC is generated per RFC 2104, using SHA-256 as the hash algorithm “H”, using the JWS Signing Input as the “text” value, and using the shared key. The HMAC output value is the JWS Signature.

In simple terms, using HS265 as the signing algorithm produces a hash-based signature, using a secret key and cryptographic hashing function to generate a MAC (message authentication code). Anyone that has access to the secret key can sign a JWT. Because of this, it’s not possible to know who created and signed a JWT, just that it was created and signed with the secret key.

As this is essentially a shared private key, it’s important to ensure that the key is adequately complex to defend against dictionary or brute-force attacks.

This website generates JWTs that have been signed by a very weak secret key. Below is a generated JWT and it’s contents:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2NTAwMjczNTAsImxldmVsIjoidXNlciIsInVzZXIiOiJqYXNwZXIifQ.FksDDlx-lIYCfa3_Y1FJdi0t55Mbmu0YGggFNVHPVJM
{
  "alg": "HS256",
  "typ": "JWT"
}
{
  "iat": 1650027350,
  "level": "user",
  "user": "jasper"
}

The website also lets us test the JWT, validating it and showing the contents.

We can see that we have successfully “logged in” as Jasper, with a level of “user”.

Using jwt_tool I can attempt to find the secret key that was used to sign the JWT via a dictionary attack, using a list of commonly used passwords:

python3 jwt_tool.py eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2NTAwMjczNTAsImxldmVsIjoidXNlciIsInVzZXIiOiJqYXNwZXIifQ.FksDDlx-lIYCfa3_Y1FJdi0t55Mbmu0YGggFNVHPVJM -C -d ../SecLists/Passwords/Common-Credentials/10k-most-common.txt

After a second or two, the key is found:

Now that I have the key, I can modify the JWT however I like and sign it using the discovered secret key. jwt_tool has a nice interactive tampering option, which allows you to add, remove, and edit fields.

python3 jwt_tool.py eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2NTAwMjczNTAsImxldmVsIjoidXNlciIsInVzZXIiOiJqYXNwZXIifQ.FksDDlx-lIYCfa3_Y1FJdi0t55Mbmu0YGggFNVHPVJM -T -S hs256 -p "hello"

Above I changed the “level” field within the claims from “user” to “admin”.

Submitting the edited and signed JWT on the website shows that I’m now “logged in” as Jasper, but this time with a level of “admin”.

As defined in section 3.2 of RFC 7518:

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

One character is eight bits, thus a secret key should be at least 32 characters. Auth0 secret keys, for example, are 512 bits in length (64 characters).

“alg”: “RS256”

“alg”: “ES256”

“alg”: “none”

As defined in section 3.6 of RFC 7518:

JWSs MAY also be created that do not provide integrity protection. Such a JWS is called an Unsecured JWS. An Unsecured JWS uses the “alg” value “none” and is formatted identically to other JWSs, but MUST use the empty octet sequence as its JWS Signature value. Recipients MUST verify that the JWS Signature value is the empty octet sequence.

As the RFC suggests above, you do not need to sign a JWT, but in most cases (if not all cases) you should sign a JWT. In fact, the RFC continues with:

Implementations MUST NOT accept Unsecured JWSs by default.

Implementations MUST NOT accept unsigned JWTs by default, as this is a common attack. Take for instance, the following JWT:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2NTAwMjczNTAsImxldmVsIjoidXNlciIsInVzZXIiOiJqYXNwZXIifQ.FksDDlx-lIYCfa3_Y1FJdi0t55Mbmu0YGggFNVHPVJM
{
  "alg": "HS256",
  "typ": "JWT"
}
{
  "iat": 1650027350,
  "level": "user",
  "user": "jasper"
}

Generating a new unsigned JWT that has the same structure can be done quite easily using CyberChef.

eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJpYXQiOjE2NTAwMjczNTAsImxldmVsIjoidXNlciIsInVzZXIiOiJqYXNwZXIifQ.
{
  "alg": "none",
  "typ": "JWT"
}
{
  "iat": 1650027350,
  "level": "user",
  "user": "jasper"
}

Notice that the JWT does not end with a signature section, but rather ends with a period ('.') character, essentially an empty signature.

A system that accepts unsigned JWTs when it should only accept signed JWTs is a system that can exploited. This is sometimes known as a downgrade attack.

JWKS and spoofing keys

The “kid” claim

JWT RFC - The JSON Web Token specification.

JSON Web Algorithms RFC - The JWS, JWE, and JWK cryptographic algorithms specification.

JWT Best Practices - Best practices for implementing and deploying JWTs.

JWT debugger - Decode, verify, and generate JWTs.

JWT, JWS and JWE for Not So Dummies! - An in-depth overview on the difference between JWT, JWS, and JWE.

Hacking JSON Web Tokens (JWT) - An overview of common JWT vulnerabilities.

OWASP JWT cheat sheet - A cheatsheet from OWASP, that says “for Java” but the information contained within is general enough.

JWT Attack Methodology - Attacking JWTs, a step by step guide.

Automated JWT assessments - Running automated assessments with JWT_Tool

>> Home