Cracking JWT tokens

a tale of magic, Node.js and parallel computing

London - 11 JUL 2018

Luciano Mammino (@loige)

Luciano... who?

Visit my castles:

- Twitter (@loige)

- GitHub (lmammino)

- Linkedin

- Blog (loige.co)

Solution Architect at

Based on prior work

Agenda

What's JWT

How it works

Testing JWT tokens

Brute-forcing a token!

 

is a compact, URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JSON object that is used as the payload of a JSON Web Signature (JWS) structure or as the plaintext of a JSON Web Encryption (JWE) structure, enabling the claims to be digitally signed or integrity protected with a Message Authentication Code (MAC) and/or encrypted.

JSON Web Token (JWT)

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXNzYWdlIjoiaGVsbG8gcGVvcGxlIn0.II7XQbjvDCHkt3UOh6weHY6tRcemT0gxRVmA6W6uZ8A

OK

Let's try to make it simpler...

JWT is...

An URL safe, stateless protocol for transferring claims

URL safe?

stateless?

claims?

URL Safe...

It's a string that can be safely used as part of a URL

(it doesn't contain URL separators like "=", "/", "#" or "?")

unicorntube.pl/?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Stateless?

Token validity can be verified without having to interrogate a third-party service

(Sometimes also defined as "self-contained")

What is a claim?

some information to transfer

identity (login session)

authorisation to perform actions (api key)

ownership (a ticket belongs to somebody)

also...

validity constraints

token time constraints (dont' use before/after)

audience (a ticket only for a specific concert)

issuer identity (a ticket issued by a specific reseller)

also...

protocol information

Type of token

Algorithm

In general

All the bits of information transferred with the token

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXNzYWdlIjoiaGVsbG8gcGVvcGxlIn0.II7XQbjvDCHkt3UOh6weHY6tRcemT0gxRVmA6W6uZ8A

3 parts

separated by "."

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXNzYWdlIjoiaGVsbG8gcGVvcGxlIn0.II7XQbjvDCHkt3UOh6weHY6tRcemT0gxRVmA6W6uZ8A

HEADER: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

PAYLOAD: eyJtZXNzYWdlIjoiaGVsbG8gcGVvcGxlIn0

SIGNATURE:

II7XQbjvDCHkt3UOh6weHY6tRcem
T0gxRVmA6W6uZ8A

Header and Payload are Base64Url encoded

let's decode them!

HEADER:

The decoded info is JSON!

PAYLOAD:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

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

eyJtZXNzYWdlIjoiaGVsbG8gcGVvc
GxlIn0

{"message":"hello people"}

HEADER:

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

alg: the kind of algorithm used

  • "HS256" HMACSHA256 Signature (secret based hashing)

  • "RS256" RSASHA256 Signature (public/private key hashing)

  • "none" NO SIGNATURE! (This is "infamous")

PAYLOAD:

{"message":"hello people"}

 

Payload can be anything that

you can express in JSON

PAYLOAD:

"registered" (or standard) claims:

iss: issuer ID ("auth0")

sub: subject ID ("johndoe@gmail.com")

aud: audience ID ("https://someapp.com")

exp: expiration time ("1510047437793")

nbf: not before ("1510046471284")

iat: issue time ("1510045471284")

PAYLOAD:

"registered" (or standard) claims:

{
  "iss": "auth0",
  "sub": "johndoe@gmail.com",
  "aud": "https://someapp.com",
  "exp": "1510047437793",
  "nbf": "1510046471284",
  "iat": "1510045471284"
}

So far it's just metadata...

What makes it safe?

SIGNATURE:

II7XQbjvDCHkt3UOh6weHY6
tRcemT0gxRVmA6W6uZ8A

 

A Base64URL encoded cryptographic signature of the header and the payload

With HS256

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

header

payload

secret

SIGNATURE

+

+

=

If a system knows the secret

It can verify the authenticity
of the token

With HS256

Let's create a token from scratch

JWT.io

Playground for JWT

An example

Session token

Classic implementation

cookie/session based

 

 

 

 

 

 

Browser

 

 

 

 

 

 

 

1. POST /login

2. generate session

id:"Y4sHySEPWAjc"
user:"luciano"
user:"luciano"
pass:"mariobros"

3. session cookie

SID:"Y4sHySEPWAjc"

4. GET /profile

5. query

id:"Y4sHySEPWAjc"

6. record

id:"Y4sHySEPWAjc"
user:"luciano"

7. (page)

<h1>hello luciano</h1>

 

 

 

 

 

 

Server

 

 

 

 

 

 

 

 

 

 

 

 

 

Sessions
Database

 

 

 

 

 

 

id:"Y4sHySEPWAjc"
user:"luciano"
SID:"Y4sHySEPWAjc"

JWT implementation

 

 

 

 

 

 

Browser

 

 

 

 

 

 

 

1. POST /login

3. JWT Token

{"sub":"luciano"}
user:"luciano"
pass:"mariobros"

6. (page)

<h1>hello luciano</h1>

 

 

 

 

 

 

Server

 

 

 

 

 

 

 

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJsdWNpYW5vIn0.V92iQaqMrBUhkgEAyRaCY7pezgH-Kls85DY8wHnFrk4

4. GET /profile

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJsdWNpYW5vIn0.V92iQaqMrBUhkgEAyRaCY7pezgH-Kls85DY8wHnFrk4
  • Token says this is "luciano"
  • Signature looks OK

5. verify

  • Create Token for "luciano"
  • Add signature

2. create
JWT

Note: Only the server knows the secret

Cookie/session

  • Needs a database to store the session data
  • The database is queried for every request to fetch the session
  • A session is identified only by a randomly generated string (session ID)
  • No data attached
  • Sessions can be invalidated at any moment

JWT

  • Doesn't need a session database
  • The session data is embedded in the token
  • For every request the token signature is verified
  • Attached metadata is readable
  • Sessions can't be invalidated, but tokens might have an expiry flag

VS

Another great JWT use case

Creating Secure Password Reset Links

JWT LOOKS GREAT!

But there are pitfalls...

Data is public!

If you have a token,

you can easily read the claims!

You only have to Base64Url-decode the token header and payload

and you have a readable JSON

There's no token database...

 

...if I can forge a token nobody will know it's not authentic!

DEMO

JWT based web app

BUILT WITH

Given an HS256 signed JWT

We can try to "guess" the secret!

How difficult can it be?

Let's build a distributed JWT token cracker!

 

npm.im/distributed-jwt-cracker

The idea...

YOU CAN NOW CREATE AND SIGN

ANY JWT TOKEN FOR THIS APPLICATION!

if the token is validated, then you found the secret!

try to "guess" the secret and validate the token against it

Take a valid JWT token

Magic weapons

Node.js

jsonwebtoken

module

ZeroMQ

an open source embeddable networking library and a concurrency framework

The brute force problem

"virtually infinite" solutions space

all the strings (of any length) that can be generated within a given alphabet

(empty string), a, b, c, 1, aa, ab, ac, a1, ba, bb, bc, b1, ca, cb, cc, c1, 1a, 1b, 1c, 11, aaa, aab, aac, aa1, aba, ...

bijection (int) ⇒ (string)

if we sort all the possible strings over an alphabet

 

Alphabet = [a,b]

0 ⟶ (empty string)
1 ⟶ a
2 ⟶ b
3 ⟶ aa
4 ⟶ ab
5 ⟶ ba
6 ⟶ bb
7 ⟶ aaa
8 ⟶ aab
9 ⟶ aba
10 ⟶ abb
11 ⟶ baa
12 ⟶ bab
13 ⟶ bba
14 ⟶ bbb
15 ⟶ aaaa
16 ⟶ aaab
17 ⟶ aaba
18 ⟶ aabb
...

Architecture

Server

Client

  • Initialised with a valid JWT token and an alphabet
  • coordinates the brute force attempts among connected clients
  • knows how to verify a token against a given secret
  • receives ranges of secrets to check

Networking patterns

Router channels:

  • dispatch jobs
  • receive results
     

Pub/Sub channel:

  • termination
    signal

Server state

the solution space can be sliced into chunks of fixed length (batch size)

0

...

batch 1

batch 2

batch 3

3

6

9

...

Initial server state

{
  "cursor": 0,
  "clients": {}
}

The first client connects

{
  "cursor": 3,
  "clients": {
    "client1": [0,2]
  }
}
[0,2]

Other clients connect

{
  "cursor": 9,
  "clients": {
    "client1": [0,2],     
    "client2": [3,5],
    "client3": [6,8]
  }
}
[0,2]
[3,5]
[6,8]

Client 2 finishes its job

{
  "cursor": 12,
  "clients": {
    "client1": [0,2],     
    "client2": [9,11],
    "client3": [6,8]
  }
}
[0,2]
[9,11]
[6,8]
let cursor = 0
const clients = new Map()

const assignNextBatch = client => {
  const from = cursor
  const to = cursor + batchSize - 1
  const batch = [from, to]
  cursor = cursor + batchSize
  client.currentBatch = batch
  client.currentBatchStartedAt = new Date()

  return batch
}

const addClient = channel => {
  const id = channel.toString('hex')
  const client = {id, channel, joinedAt: new Date()}
  assignNextBatch(client)
  clients.set(id, client)

  return client
}

Server

Messages flow

 

 

 

JWT Cracker

Server

 

 

 

 

 

 

 

JWT Cracker

Client

 

 

 

 

1. JOIN

2. START

{token, alphabet, firstBatch}

3. NEXT

4. BATCH

{nextBatch}

5. SUCCESS

{secret}
const router = (channel, rawMessage) => {
  const msg = JSON.parse(rawMessage.toString())

  switch (msg.type) {
    case 'join': {
      const client = addClient(channel)
      const response = {
        type: 'start',
        id: client.id,
        batch: client.currentBatch,
        alphabet,
        token
      }
      batchSocket.send([channel, JSON.stringify(response)])
      break
    }

    case 'next': {
      const batch = assignNextBatch(clients.get(channel.toString('hex')))
      batchSocket.send([channel, JSON.stringify({type: 'batch', batch})])
      break
    }

    case 'success': {
      const secret = msg.secret
      // publish exit signal and closes the app
      signalSocket.send(['exit', JSON.stringify({secret, client: channel.toString('hex')})], 0, () => {
        batchSocket.close()
        signalSocket.close()
        exit(0)
      })

      break
    }
  }
}

Server

let id, variations, token

const dealer = rawMessage => {
  const msg = JSON.parse(rawMessage.toString())

  const start = msg => {
    id = msg.id
    variations = generator(msg.alphabet)
    token = msg.token
  }

  const batch = msg => {
    processBatch(token, variations, msg.batch, (secret, index) => {
      if (typeof secret === 'undefined') {
        // request next batch
        batchSocket.send(JSON.stringify({type: 'next'}))
      } else {
        // propagate success
        batchSocket.send(JSON.stringify({type: 'success', secret, index}))
        exit(0)
      }
    })
  }

  switch (msg.type) {
    case 'start':
      start(msg)
      batch(msg)
      break

    case 'batch':
      batch(msg)
      break
  }
}

Client

How a chunk is processed

Given chunk [3,6] over alphabet "ab"

[3,6] ⇒

3 ⟶ aa
4 ⟶ ab
5 ⟶ ba
6 ⟶ bb

check if one of the strings is the secret that validates the current token

const jwt = require('jsonwebtoken')
const generator = require('indexed-string-variation').generator;
const variations = generator('someAlphabet')

const processChunk = (token, from, to) => {
  let secret

  for (let i = from; i < to; i++) {
    try {
      secret = variations(i)

      jwt.verify(token, secret, {
        ignoreExpiration: true,
        ignoreNotBefore: true
      })

      // finished, password found
      return ({found: secret})
    } catch (err) {} // password not found, keep looping
  }
  
  // finished, password not found
  return null
}

Client

Demo

Closing off

Is JWT safe to use?

Definitely
YES!

Heavily used by:

but...

Use a strong (≃long) secret and keep it SAFE!

Or, even better

Use RS256 (RSA public/private key pair) signature

Use it wisely!

But, what if I create only
short lived tokens...

JWT is STATELESS!

the expiry time is contained in the token...

if you can edit tokens, you can extend the expiry time as needed!

Should I be worried about

brute force?

Some benchmarks

Secret length Attempts Time*
5 26^5 =  12 mln 1m55.618s
6 26^6 = 310 mln 1h32m48.378s
7 26^7 = 8 Bln 3d18h22m10.231s
8 26^8 = 210 Bln ⏱ still running...
9 26^9 = 5.5 Tln 😨I didn't even dare to try!

* 8 clients on 4 t2.medium AWS machines, [a-z] alphabet (loige.link/jwt-bench)

If you use a 64 chars  secret...

26^64 =
3.616.548.304.479.297.085.365.330.736.464.680.499.909.051.895.704.748.593.486.634.912.486.670.341.490.423.472.351.870.976

3.6 GAZILLION attempts needed!

TLDR;

JWT is a cool & stateless™ way to transfer claims!

 

Choose the right Algorithm

With HS256, choose a good secret and keep it safe

Don't disclose sensitive information in the payload

Don't be too worried about brute force, but understand how it works!

Thanks!

Credits

vector images designed by freepik

an      heartfelt thank you to:

@mariocasciaro

@AlleviTommaso

@andreaman87

@cirpo

@katavic_d

@Podgeypoos79

@quasi_modal

"the amazing" Paul Dolan