How to exploit JSON web tokens

Author:
Published on:

How to discover and exploit weak implementation of Json web tokens.

André Eichhofer

Knowing at least the foundation on how to exploit weak Json web tokens is key to every auditor when assessing the security of cloud applications. For example, the BSI-C5 catalogue requires a robust session management, similar to Fisma 800 - 53 or FedRAMP.

Before diving into the topic make yourself familiar on how Json web tokens work

A JSON web token (jwt) is generated server side to authenticate users. After it is generated, the server passes the token to the user in the response header.

Example response header

Set cookie: AuthToken=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Then the browser safes the token as a cookie and passes the token each time in the GET request to the server when the user accesses a resource.

You can manipulate the token or try to brute-force the key to get unauthorised access. Most JWT attacks are privilege escalations as you already must have a valid JWT. The goal is to manipulate the token to get elevated access.

Discover Jwts

Shodan searches the headers of services and this way you can discover JWTs.

Search for terms in HTTP response headers (examples):

  • jwt
  • "jwt=", jwt_cookie=
  • token
  • session_token

Search for base64 encoded JWT headers

  • eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9: search for “HS256” encrypted tokens
  • eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9: search for “RS256” encrypted tokens

Jwt attacks

All attacks aim to manipulate the payload of the JWT in order to get elevated access. To manipulate the payload you must break the signing key. Also, you must have a valid JWT that gives you (unprivileged) access and information about the payload. So, the steps would be as follows:

  1. Have an unprivileged JWT
  2. Break signing key
    • brute force signing key
    • flawed signatures
    • header parameter injections
  3. Manipulate the payload

Weak signing key

The attack path to exploit a weak signing key is:

get a valid JWT -> brute force signing key -> change the payload -> resign the JWT

At first, obtain a valid JWT and inspect it in any decoder, for example:

Set cookie: AuthToken=eyJraWQiOiIyOWFmMzJmMi1mODA2LTQ0ZGYtOTAxYi0zZWYwZTRkMjUxZjQiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsInN1YiI6IndpZW5lciIsImV4cCI6MTcwNTA2ODQ2OX0.ZrhSie1csmYWGKLX8Mt4iGbp0bRWW9MuGvM1B6KLPA0

Example output

HEADER:

{
  "kid": "29af32f2-f806-44df-901b-3ef0e4d251f4",
  "alg": "HS256"
}

PAYLOAD:

{
  "iss": "portswigger",
  "sub": "wiener",
  "exp": 1705068469
}

SIGNATURE

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

The goal is to change the user "wiener" to "admin" to gain elevated privileges. However, as the JWT is signed with a secret key you cannot just change the payload (the JWT would be invalid and rejected by the server). Therefore, you need to crack the secret key.

You can use several tools to crack the signature with a wordlist:

  • hashcat -a 0 -m 16500 <jwt> <wordlist>: cracking with hashcat
  • python3 jwt_tool.py <jwt> -C -d <wordlist>: cracking with jwt_tool

After you have cracked the key you can generate a new token with any tool, such as

  • python3 jwt_tool <jwt> -S <algorithm> -p <secret_key> -T: tamper token and generate new signed token
  • Burpsuite JWT extensions
  • http://jwt.io

After you have generated a new token you can insert the tampered token in the GET request using Burpsuite or insert the token in you browser to gain access to the resource.

Flawed JWT signature verification


Accepting arbitrary signatures

JWT libraries typically provide one method for verifying tokens and another to decode them. For example, Node.js library jsonwebtoken has verify() and decode(). The source code first verifies the signature of the token and then decodes the payload.

Sometimes, developers confuse these methods and only pass tokens to the decode() method. This means that the application does not verify the signature at all.

In this case you can just tamper the payload without resigning the token. The attack path is simply:

get valid JWT -> tamper payload -> privilege escalation

Accepting tokens with no signature

Usaually, JWT tokens are signed with a special signing algorithm. The signing algorithm is defined in the header of the token:

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

The user could tamper the "alg" parameter to "alg": "none" which means that the token is not signed at all. Therefore, servers are programmed to reject unsigned tokens. However, the filtering relies on string parsing and you can sometimes bypass these filters using classic obfuscation techniques, such as mixed capitalization and unexpected encodings.

Example obfuscation techniques:

  • none
  • None
  • nOne
  • etc.

If the server accepts a non-signed JWT you can tampler the token to perform privilege escalation, for example

  • "user": "administrator"

How to test a token for alg:none vulnerability with jwt_tool:

  • Remove the signature from the original, unprivileged token using the alg:none exploit (-X a)
  • Test whether the server accepts the unsigned, unprivileged token
  • Tamper the original, unprivileged and signed token and change the user payload (-T)
  • Remove the signature from the tampered token using the alg:none exploit

JWT header parameter injections

A JWT header can contain additional parameters, like

  • jwk: (JSON Web Key) - embedded JSON object representing the key
  • jku: (JSON Web Key Set URL) - Provides a URL from which servers can fetch a set of keys containing the correct key
  • kid: (Key ID) - Provides an ID that servers can use to identify the correct key in cases where there are multiple keys to choose from

JWK header injection

The jwk header is used to store a public key within the header itself. This public key is used to verify the token which was signed with a private key.

{
    "kid": "ed2Nf8sb-sD6ng0-scs5390g-fFD8sfxG",
    "typ": "JWT",
    "alg": "RS256",
    "jwk": {
        "kty": "RSA",
        "e": "AQAB",
        "kid": "ed2Nf8sb-sD6ng0-scs5390g-fFD8sfxG",
        "n": "yy1wpYmffgXBxhAUJzHHocCuJolwDqql75ZWuCQ_cb33K2vh9m"
    }
}

Servers should only use a limited list of public keys to verify JWT signatures. However, misconfigured servers sometimes use any key that’s embedded in the jwk parameter. An attacker can therefore create his own public-private key pair, tamper the token, and sign the token with hos own private key.

Attack path:

create a public private key pair -> tamper token -> embed own public key in jwk parameter -> sign token with own private key

Test jwk header injection with jwt_tool (Example):

  • Use “inject inline jwks” exploit and tamper token (-X i -T)
  • Change “kid” parameter to “jwt_tool” (otherwise server may not accept token, because of different “kid’s”)
  • Change username to “administrator”

Jwt_tool will tamper the token, generate a public-private key pair, embed the public key in the jwk header and sign the token with the private key.

JKU header injection

The jku header is used to store a url containing the public key in the token. To verify the token the server fetches the public key from the remote url.

Header with url to remote host

{
  "kid": "9ab0f248-9d4c-4505-88f0-50a19d21326b",
  "alg": "RS256",
  "jku": "https://exploit-server.com/public_key"
}

Header with public key on the remote host

{
    "keys":[
        {
            "kty":"RSA",
            "kid":"jwt_tool",
            "use":"sig",
            "e":"AQAB",
            "n":"jK4wC4AO9dX77XTwaL6fWmO1U0AFxc4sd1cgEcCL_WQ2LYMWrVytxXPzvjvO4c2gmhgNXmnXWuNoTiwYfPWy0H2pqe8iXIGyHQht9MyTdXmXYugFna_Se41ivbGImjiRStU4i7eO2z28pTvEfMRL3tC_cbaiiIj3pcyyEHSd6ua99XFcE9vyM4GJBfATTVckGerimiRkGgXzgEWl22MpJVoK5oFhJShzP84dY6QEjDYr0M3ZBgoZRv7fB7hqDt6rgD-Akf2YzlgJ_ZqLTPvlyx0ROPxn85WWTxdu0wNe_ObQv5CBSuD51SQaatXG5AEhHDfG7vp5rGAFF6an7KFq8w"
        }
    ]
}

Servers should only fetch keys from trusted domains, but sometimes there are misconfigured and allow untrusted domains.

Attack path:

  1. Create a public private key pair
  2. Setup a remote server
  3. Put the header containing the public key on the server
  4. Tamper the token

Test jku header injection with jwt_tool (Example):

  • Use “spoof jwks” exploit and tamper token (-X s -ju <url> -T)
  • Change “kid” parameter to “jwt_tool” (otherwise server may not accept token, because of different “kid’s”)
  • Change username to “administrator”
  • Copy the public key header from jwttool_custom_jwks.json to the remote server

Jwt_tool will tamper the token, generate a public-private key pair, output the public key header to a file and sign the token with the private key.

KID header injection

The kid parameter usually stores information, that tells the server which key to use to verify the token (e.g. database entry, any ID that points to the key). The key can also be the content of a file on the server

If the kid parameter is vulnerable to path traversal you can generate an arbitrary signature, by

  • pointing the path to a static file on the remote server and generating your own key if you know the content of the file
  • pointing the path to /dev/null and generating an unsigned token (dev/null always points to zero).
{
    "kid": "../../path/to/file",
    "typ": "JWT",
    "alg": "HS256",
    "k": "asGsADas3421-dfh9DGN-AFDFDbasfd8-anfjkvc"
}

Note: You cannot use the attack for a classic path traversal as the “kid” parameter only points to the key, but does not reflect any content.

Attack path:

  1. The kid parameter is vulnerable to path traversal
  2. Algorithm is symmetric
  3. Tamper the token
  4. Set kid parameter to “/dev/null”

Test kid header injection with jwt_tool (Example):

  • Change “kid:” to “../../../dev/null”
  • Change username to “administrator”
  • Sign the token with HS256 and set an empty password (-S hs256 -P '')

book.hacktricks.xyz/pentesting-web/hacking-jwt-json-web-tokens