Cracking JWT tokens

a tale of magic, Node.js and parallel computing

Node.js Dublin

 30 NOV 2017

Luciano Mammino (@loige)

Luciano... who!?

Visit my castle:

 

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!

 

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.eyJtZXNzYWdlIjoiaGVsbG8gZHVibGluIn0.3XcG-nyWravBScxDH1amc7-APwEq6H1eEAM_6PV9umc

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

(Sometimes also defined as "self-contained")

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

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXNzYWdlIjoiaGVsbG8gZHVibGluIn0.3XcG-nyWravBScxDH1amc7-APwEq6H1eEAM_6PV9umc

3 parts

separated by "."

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXNzYWdlIjoiaGVsbG8gZHVibGluIn0.3XcG-nyWravBScxDH1amc7-APwEq6H1eEAM_6PV9umc

HEADER: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

PAYLOAD: eyJtZXNzYWdlIjoiaGVsbG8gZHVibGluIn0

SIGNATURE:

3XcG-nyWravBScxDH1amc7-APwEq6H1eEAM_6PV9umc

Header and Payload are Base64Url encoded

let's decode them!

HEADER:

The decoded info is JSON!

PAYLOAD:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

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

eyJtZXNzYWdlIjoiaGVsbG8gZHVibGluIn0

{"message":"hello dublin"}

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 dublin"}

 

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:

3XcG-nyWravBScxDH1amc7-APwEq6H1eEAM_6PV9umc

 

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

With HS256

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

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

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

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...

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)

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 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++) {
    try {
      pwd = variations(i)

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

      // finished, password found
      return ({found: pwd})
    } 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 strong (≃long) passwords and keep them 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?

Not really

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

TLDR;

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

 

Choose the right Algorithm

With HS256, choose a good password and keep it safe

Don't disclose sensible information in the payload

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

{"THANK":"YOU"}

Credits

vector images designed by freepik

Cracking JWT tokens: a tale of magic, Node.js and parallel computing - Node.js Dublin November 2017

By Luciano Mammino

Cracking JWT tokens: a tale of magic, Node.js and parallel computing - Node.js Dublin November 2017

Learn how you can use some JavaScript/Node.js black magic to crack JWT tokens and impersonate other users or escalate privileges. Just add a pinch of ZeroMQ, a dose of parallel computing, a 4 leaf clover, mix everything applying some brute force and you'll get a powerful JWT cracking potion!

  • 294
Loading comments...

More from Luciano Mammino