Cracking JWT tokens: a tale of magic, Node.js and parallel computing

CODEMOTION MILAN - SPECIAL EDITION  10 - 11 NOVEMBER 2017

Luciano Mammino (@loige)

About Luciano

Let's connect:

 

Twitter - GitHub - Linkedin

 

https://loige.co

Principal Application Engineer

Based on prior work

Agenda

What's JWT

How it works

Testing JWT tokens

Brute-forcing a token!

 

JSON Web Token (JWT)

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.

 

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXNzYWdlIjoiaGVsbG8gY29kZW1vdGlvbiJ9.LfQ4AOIjQPeAotn237m5yiMgJacC_00ePvlFC4fyRXE

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 "?")

Stateless?

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

What is a claim?

some certified information

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 through the token

 

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXNzYWdlIjoiaGVsbG8gY29kZW1vdGlvbiJ9.LfQ4AOIjQPeAotn237m5yiMgJacC_00ePvlFC4fyRXE

 

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXNzYWdlIjoiaGVsbG8gY29kZW1vdGlvbiJ9.LfQ4AOIjQPeAotn237m5yiMgJacC_00ePvlFC4fyRXE

 

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXNzYWdlIjoiaGVsbG8gY29kZW1vdGlvbiJ9.LfQ4AOIjQPeAotn237m5yiMgJacC_00ePvlFC4fyRXE

3 parts

separated by "."

HEADER: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

PAYLOAD: eyJtZXNzYWdlIjoiaGVsbG8gY29kZW1vdGlvbiJ9

SIGNATURE: LfQ4AOIjQPeAotn237m5yiMgJacC_00ePvlFC4fyRXE

Header and Payload are Base64Url encoded

let's decode them!

HEADER:

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

The decoded info is JSON!

PAYLOAD:

{"message":"hello codemotion"}

HEADER:

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

 

alg: the kind of algorithm used

  • "HS256" HMACSHA256 Signature
     (secret based hashing)
     

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

PAYLOAD:

{"message":"hello codemotion"}

 

Payload can be anything 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")

  • jti: Unique identifier ("36c56616-2125-4a6e-b333-bc8327bd39d6")

So far it's just metadata...

What makes it safe?

SIGNATURE: LfQ4AOIjQPeAotn237m5yiMgJacC_00ePvlFC4fyRXE

 

A Base64URL encoded cryptographic signature of the header and the payload

With HS256

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

header

payload

secret

SIGNATURE

+

+

=

If a system knows the secret

It can verify the authenticity
of the token

JWT.io

Playground for JWT

An example

Session token

Classic implementation

Without JWT

 

 

 

 

 

 

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

(NO session database)

 

 

 

 

 

 

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

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

No token database...

 

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

DEMO

JWT based web app

Given an HS256 signed JWT

We can try to "guess" the password!

How difficult can it be?

Let's build a distributed JWT token cracker!

 

npm.im/distributed-jwt-cracker

The idea...

Take a valid JWT token

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

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

YOU CAN NOW CREATE AND SIGN ANY JWT TOKEN FOR THIS APPLICATION!

Tools of the trade

Node.js

ZeroMQ

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

  • Initialised with a valid JWT token and an alphabet
  • coordinates the brute force attempts among connected clients

Client

  • 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)

Initial server state

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

The first client connects

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

Other clients connect

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

Client 2 finishes its job

{
  "cursor": 12,
  "clients": {
    "client1": [0,2],     
    "client2": [9,11],
    "client3": [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 pwd = msg.password
      // publish exit signal and closes the app
      signalSocket.send(['exit', JSON.stringify({password: pwd, 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, (pwd, index) => {
      if (typeof pwd === 'undefined') {
        // request next batch
        batchSocket.send(JSON.stringify({type: 'next'}))
      } else {
        // propagate success
        batchSocket.send(JSON.stringify({type: 'success', password: pwd, 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 pwd

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

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

    // finished, password found
    return ({found: i})
  }
  
  // finished, password not found
  return null
}

Client

Demo

Closing off

Is JWT safe to use?

Definitely
YES!

but...

Use strong (≃long) passwords and keep them SAFE!

Or, even better

Use RS256 (RSA public/private key pair) signature

Use it wisely!

Should I be worried about brute force?

Not really

... As long as you know the basic rules
(and the priorities) to defend yourself

A challenge for you:

Can you crack this one?

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjcmFjayI6Im1lIiwiaWYiOiJ5b3UgY2FuIn0.tI8zO0gj6WBgaVoKNeHwCKOxOlr3Jo7OqKHwMgrqJJE

If you can, tweet the secret to @loige
I have a prize for the first one!

{"THANK":"YOU"}