Building a JWT Token Cracker with ZeroMQ & Node.js (Part 2.)

This is the second episode of a two-part tutorial. While the first article (ZeroMQ & Node.js Tutorial - Cracking JWT Tokens) was solely focused on theory, this one is about the actual coding.

You’ll get to know ZeroMQ, how JWT tokens work and how our application can crack some of them! Be aware, that the application will be intentionally simple. I only want to demonstrate how we can leverage some specific patterns.

At the end of the article, I’ll invite you to participate in a challenge and to use your newly acquired knowledge for cracking a JWT token. The first 3 developers who crack the code will get a gift!

Let’s get it started!

Preparing the environment and the project folder

To follow this tutorial, you will need to have the ZeroMQ libraries and Node.js version >=4.0 installed in your system. We will also need to initialize a new project with the following commands:

npm init # then follow the guided setup  
npm install --save big-integer@^1.6.16 dateformat@^1.0.12 indexed-string-variation@^1.0.2 jsonwebtoken@^7.1.9 winston@^2.2.0 [email protected] zmq@^2.15.3  

This will make sure that you have all the dependencies ready in the project folder and you can only focus on the code.

You can also checkout the code in the projects’ official GitHub repository and keep it aside as a working reference.

Writing the client application (Dealer + Subscriber) with ZeroMQ and Node.js

We should finally have a clear understanding of the whole architecture and the patterns we are going to use. Now we can finally focus on writing code!

Let's start with the code representing the client, which holds the real JWT-cracking business logic.

As a best practice, we are going to use a modular approach, and we will split our client code into four different parts:

  • The processBatch module, containing the core logic to process a batch.
  • The createDealer module containing the logic to handle the messages using the ZeroMQ dealer pattern.
  • The createSubscriber module containing the logic to handle the exit message using the subscriber pattern.
  • The client executable script that combines all the modules together and offers a nice command-line interface.

The processBatch module

The first module that we are going to build will focus only on analyzing a given batch and checking if the right password is contained in it.

This is probably the most complex part of our whole application, so let's make some useful preambles:

  • We are going to use the big-integer library to avoid approximation problems with large integers. In fact, in JavaScript all numbers are internally represented as floating point numbers and thus they are subject to floating point approximation. For example the expression 10000000000000000 === 10000000000000001 (notice the last digit) will evaluate to true. If you are interested in this aspect of the language, you can read more here](http://greweb.me/2013/01/be-careful-with-js-numbers/). All the maths in our project will be managed by the big-integer library. If you have never used it before, it might look a bit weird at first, but I promise it won't be hard to understand.
  • We are also going to use the jsonwebtoken library to verify the signature of a given token against a specific password.

Let's finally see the code of the processBatch module:

// src/client/processBatch.js

'use strict';

const bigInt = require('big-integer');  
const jwt = require('jsonwebtoken');

const processBatch = (token, variations, batch, cb) => {  
  const chunkSize = bigInt(String(1000));

  const batchStart = bigInt(batch[0]);
  const batchEnd = bigInt(batch[1]);

  const processChunk = (from, to) => {
    let pwd;

    for (let i = from; i.lesser(to); i = i.add(bigInt.one)) {
      pwd = variations(i);
      try {
        jwt.verify(token, pwd, {ignoreExpiration: true, ignoreNotBefore: true});
        // finished, password found
        return cb(pwd, i.toString());
      } catch (e) {}
    }

    // prepare next chunk
    from = to;
    to = bigInt.min(batchEnd, from.add(chunkSize));

    if (from === to) {
      // finished, password not found
      return cb();
    }

    // process next chunk
    setImmediate(() => processChunk(from, to));
  };

  const firstChunkStart = batchStart;
  const firstChunkEnd = bigInt.min(batchEnd, batchStart.add(chunkSize));
  setImmediate(() => processChunk(firstChunkStart, firstChunkEnd));
};

module.exports = processBatch;  

(Note: This is a slightly simplified version of the module, you can check out the original one in the official repository which also features a nice animated bar to report the batch processing progress on the console.)

This module exports the processBatch function, so first things first, let's analyze the arguments of this function:

  • token: The current JWT token.
  • variations: An instance of indexed-string-variations already initialized with the current alphabet.
  • batch: An array containing two strings representing the segment of the solution space where we search for the password (e.g. ['22', '150']).
  • cb: A callback function that will be invoked on completion. If the password is found in the current batch, the callback will be invoked with the password and the current index as arguments. Otherwise, it will be called without arguments.

This function is asynchronous, and it is the one that will be executed most of the time in the client.

The main goal is to iterate over all the numbers in the range, and generate the corresponding string on the current alphabet (using the variations function) for every number.

After that, the string is checked against jwt.verify to see if it's the password we were looking for. If that's the case, we immediately stop the execution and invoke the callback, otherwise the function will throw an error, and we will keep iterating until the current batch is fully analyzed. If we reach the end of the batch without success, we invoke the callback with no arguments to notify the failure.

What's peculiar here is that we don't really execute a single big loop to cover all the batch elements, but instead we define an internal function called processChunk that has the goal of executing asynchronously the iteration in smaller chunks containing at most 1000 elements.

We do this because we want to avoid to block the event loop for too long, so, with this approach, the event loop has a chance to react to some other events after every chunk, like a received exit signal.

(You can read much more on this topic in the last part of Node.js Design Patterns Second Edition).

CreateDealer module

The createDealer module holds the logic that is needed to react to the messages received by the server through the batchSocket, which is the one created with the router/dealer pattern.

Let's jump straight into the code:

// src/client/createDealer.js

'use strict';

const processBatch = require('./processBatch');  
const generator = require('indexed-string-variation').generator;

const createDealer = (batchSocket, exit, logger) => {  
  let id;
  let variations;
  let token;

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

    const start = msg => {
      id = msg.id;
      variations = generator(msg.alphabet);
      token = msg.token;
      logger.info(`client attached, got id "${id}"`);
    };

    const batch = msg => {
      logger.info(`received batch: ${msg.batch[0]}-${msg.batch[1]}`);
      processBatch(token, variations, msg.batch, (pwd, index) => {
        if (typeof pwd === 'undefined') {
          // request next batch
          logger.info(`password not found, requesting new batch`);
          batchSocket.send(JSON.stringify({type: 'next'}));
        } else {
          // propagate success
          logger.info(`found password "${pwd}" (index: ${index}), exiting now`);
          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;

      default:
        logger.error('invalid message received from server', rawMessage.toString());
    }
  };

  return dealer;
};

module.exports = createDealer;  

This module exports a factory function used to initialize our dealer component. The factory accepts three arguments:

  • batchSocket: the ZeroMQ socket used to implement the dealer part of the router/dealer pattern.
  • exit: a function to end the process (it will generally be process.exit).
  • logger: a logger object (the console object or a winston logger instance) that we will see in detail later.

The arguments exit and logger are requested from the outside (and not initialized within the module itself) to make the module easily "composable" and to simplify testing (we are here using the Dependency Injection pattern).

The factory returns our dealer function which in turn accepts a single argument, the rawMessage received through the batchSocket channel.

This function has two different behaviors depending on the type of the received message. We assume the first message is always a start message that is used to propagate the client id, the token and the alphabet. These three parameters are used to initialize the dealer. The first batch is also sent with them, so after the initialization, the dealer can immediately start to process it.

The second message type is the batch, which is used by the server to deliver a new batch to analyze to the clients.

The main logic to process a batch is abstracted in the batch function. In this function, we simply delegate the processing job to our processBatch module. If the processing is successful, the dealer creates a success message for the router - transmitting the discovered password and the corresponding index over the given alphabet. If the batch doesn't contain the password, the dealer sends a next message to the router to request a new batch.

CreateSubscriber module

In the same way, we need an abstraction that allows us to manage the pub/sub messages on the client. For this purpose we can have the createSubscriber module:

// src/client/createSubscriber.js

'use strict';

const createSubscriber = (subSocket, batchSocket, exit, logger) => {  
  const subscriber = (topic, rawMessage) => {
    if (topic.toString() === 'exit') {
      logger.info(`received exit signal, ${rawMessage.toString()}`);
      batchSocket.close();
      subSocket.close();
      exit(0);
    }
  };

  return subscriber;
};

module.exports = createSubscriber;  

This module is quite simple. It exports a factory function that can be used to create a subscriber (a function able to react to messages on the pub/sub channel). This factory function accepts the following arguments:

  • subSocket: the ZeroMQ socket used for the publish/subscribe messages.
  • batchSocket: the ZeroMQ socket used for the router/dealer message exchange (as we saw in the createDealer module).
  • exit and logger: as in the createDealer module, these two arguments are used to inject the logic to terminate the application and to record logs.

The factory function, once invoked, returns a subscriber function which contains the logic to execute every time a message is received through the pub/sub socket. In the pub/sub model, every message is identified by a specific topic. This allows us to react only to the messages referring to the exit topic and basically shut down the application. To perform a clean exit, the function will take care of closing the two sockets before exiting.

Command line client script

Finally, we have all the pieces we need to assemble our client application. We just need to write the glue between them and expose the resulting application through a nice command line interface.

To simplify the tedious task of parsing the command line arguments, we will use the yargs library:

// src/client.js

#!/usr/bin/env node

'use strict';

const zmq = require('zmq');  
const yargs = require('yargs');  
const logger = require('./logger');  
const createDealer = require('./client/createDealer');  
const createSubscriber = require('./client/createSubscriber');

const argv = yargs  
  .usage('Usage: $0 [options]')
  .example('$0 --host=localhost --port=9900 -pubPort=9901')
  .string('host')
  .default('host', 'localhost')
  .alias('h', 'host')
  .describe('host', 'The hostname of the server')
  .number('port')
  .default('port', 9900)
  .alias('p', 'port')
  .describe('port', 'The port used to connect to the batch server')
  .number('pubPort')
  .default('pubPort', 9901)
  .alias('P', 'pubPort')
  .describe('pubPort', 'The port used to subscribe to broadcast signals (e.g. exit)')
  .help()
  .version()
  .argv
;

const host = argv.host;  
const port = argv.port;  
const pubPort = argv.pubPort;

const batchSocket = zmq.socket('dealer');  
const subSocket = zmq.socket('sub');  
const dealer = createDealer(batchSocket, process.exit, logger);  
const subscriber = createSubscriber(subSocket, batchSocket, process.exit, logger);

batchSocket.on('message', dealer);  
subSocket.on('message', subscriber);

batchSocket.connect(`tcp://${host}:${port}`);  
subSocket.connect(`tcp://${host}:${pubPort}`);  
subSocket.subscribe('exit');  
batchSocket.send(JSON.stringify({type: 'join'}));  

In the first part of the script we use yargs to describe the command line interface, including a description of the command with a sample usage and all the accepted arguments:

  • host: is used to specify the host of the server to connect to.
  • port: the port used by the server for the router/dealer exchange.
  • pubPort: the port used by the server for the pub/sub exchange.

This part is very simple and concise. Yargs will take care of performing all the validations of the input and populates the optional arguments with default values in case they are not provided by the user. If some argument doesn't meet the expectations, Yargs will take care of displaying a nice error message. It will also automatically create the output for --help and --version.

In the second part of the script, we use the arguments provided to connect to the server, creating the batchSocket (used for the router/dealer exchange) and the subSocket (used for the pub/sub exchange).

We use the createDealer and createSubscriber factories to generate our dealer and subscriber functions and then we associate them with the message event of the corresponding sockets.

Finally, we subscribe to the exit topic on the subSocket and send a join message to the server using the batchSocket.

Now our client is fully initialized and ready to respond to the messages coming from the two sockets.

The server

Now that our client application is ready we can focus on building the server. We already described what will be the logic that the server application will adopt to distribute the workload among the clients, so we can jump straight into the code.

CreateRouter

For the server, we will build a module that contains most of the business logic - the createRouter module:

// src/server/createRouter.js

'use strict';

const bigInt = require('big-integer');

const createRouter = (batchSocket, signalSocket, token, alphabet, batchSize, start, logger, exit) => {  
  let cursor = bigInt(String(start));
  const clients = new Map();

  const assignNextBatch = client => {
    const from = cursor;
    const to = cursor.add(batchSize).minus(bigInt.one);
    const batch = [from.toString(), to.toString()];
    cursor = cursor.add(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;
  };

  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)]);
        logger.info(`${client.id} joined (batch: ${client.currentBatch[0]}-${client.currentBatch[1]})`);
        break;
      }

      case 'next': {
        const batch = assignNextBatch(clients.get(channel.toString('hex')));
        logger.info(`client ${channel.toString('hex')} requested new batch, sending ${batch[0]}-${batch[1]}`);
        batchSocket.send([channel, JSON.stringify({type: 'batch', batch})]);
        break;
      }

      case 'success': {
        const pwd = msg.password;
        logger.info(`client ${channel.toString('hex')} found password "${pwd}"`);
        // 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;
      }

      default:
        logger.error('invalid message received from channel', channel.toString('hex'), rawMessage.toString());
    }
  };

  router.getClients = () => clients;

  return router;
};

module.exports = createRouter;  

The first thing to notice is that we built a module that exports a factory function again. This function will be used to initialize an instance of the logic used to handle the router part of the router/dealer pattern in our application.

The factory function accepts a bunch of parameters. Let's describe them one by one:

  • batchSocket: is the ZeroMQ socket used to send the batch requests to the clients.
  • signalSocket: is the ZeroMQ socket to publish the exit signal to all the clients.
  • token: the string containing the current token.
  • alphabet: the alphabet used to build the strings in the solution space.
  • batchSize: the number of strings in every batch.
  • start: the index from which to start the first batch (generally '0').
  • logger: an instance of the logger
  • exit: a function to be called to shut down the application (usually process.exit).

Inside the factory function, we declare the variables that define the state of the server application: cursor and clients. The first one is the pointer to the next batch, while the second is a map structure used to register all the connected clients and the batches assigned to them. Every entry in the map is an object containing the following attributes:

  • id: the id given by ZeroMQ to the client connection.
  • channel: a reference to the communication channel between client and server in the router/dealer exchange.
  • joinedAt: the date when the client established a connection to the server.
  • currentBatch: the current batch being processed by the client (an array containing the two delimiters of the segment of the solution space to analyze).
  • currentBatchStartedAt: the date when the current batch was assigned to the client.

Then we define two internal utility functions used to change the internal state of the router instance: assignNextBatch and addClient.

The way these functions work is pretty straightforward: the first one assigns the next available batch to an existing client and moves the cursors forward, while the second takes input a new ZeroMQ connection channel as an input and creates the corresponding entry in the map of connected clients.

After these two helper functions, we define the core logic of our router with the router function. This function is the one that is returned by the factory function and defines the logic used to react to an incoming message on the router/dealer exchange.

As it was happening for the client, we can have different type of messages, and we need to react properly to every one of them:

  • join: received when a client connects to the server for the first time. In this case, we register the client and send it the settings of the current run and assign it the first batch to process. All this information is provided with a start message, which is sent on the router/dealer channel (using the ZeroMQ batchSocket).
  • next: received when a client finishes to process a batch without success and needs a new batch. In this case we simply assign the next available batch to the client and send the information back to it using a batch message through the batchSocket.
  • success: received when a client finds the password. In this case, the found password is logged and propagated to all the other clients with an exit signal through the signalSocket (the pub/sub exchange). When the exit signal broadcast is completed, the application can finally shut down. It also takes care to close the ZeroMQ sockets, for a clean exit.

That's mostly it for the implementation of the router logic.

However, it's important to underline that this implementation is assuming that our clients always deliver either a success message or a request for another batch. In a real world application, we must take into consideration that a client might fail or disconnect at any time and manages to redistribute its batch to some other client.

The server command line

We have already written most of our server logic in the createRouter module, so now we only need to wrap this logic with a nice command line interface:

// src/server.js

#!/usr/bin/env node

'use strict';

const zmq = require('zmq');  
const isv = require('indexed-string-variation');  
const yargs = require('yargs');  
const jwt = require('jsonwebtoken');  
const bigInt = require('big-integer');  
const createRouter = require('./server/createRouter');  
const logger = require('./logger');

const argv = yargs  
  .usage('Usage: $0 <token> [options]')
  .example('$0 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ')
  .demand(1)
  .number('port')
  .default('port', 9900)
  .alias('p', 'port')
  .describe('port', 'The port used to accept incoming connections')
  .number('pubPort')
  .default('pubPort', 9901)
  .alias('P', 'pubPort')
  .describe('pubPort', 'The port used to publish signals to all the workers')
  .string('alphabet')
  .default('alphabet', isv.defaultAlphabet)
  .alias('a', 'alphabet')
  .describe('alphabet', 'The alphabet used to generate the passwords')
  .number('batchSize')
  .alias('b', 'batchSize')
  .default('batchSize', 1000000)
  .describe('batchSize', 'The number of attempts assigned to every client in a batch')
  .number('start')
  .alias('s', 'start')
  .describe('start', 'The index from where to start the search')
  .default('start', 0)
  .help()
  .version()
  .check(args => {
    const token = jwt.decode(args._[0], {complete: true});
    if (!token) {
      throw new Error('Invalid JWT token: cannot decode token');
    }

    if (!(token.header.alg === 'HS256' && token.header.typ === 'JWT')) {
      throw new Error('Invalid JWT token: only HS256 JWT tokens supported');
    }

    return true;
  })
  .argv
;

const token = argv._[0];  
const port = argv.port;  
const pubPort = argv.pubPort;  
const alphabet = argv.alphabet;  
const batchSize = bigInt(String(argv.batchSize));  
const start = argv.start;  
const batchSocket = zmq.socket('router');  
const signalSocket = zmq.socket('pub');  
const router = createRouter(  
  batchSocket,
  signalSocket,
  token,
  alphabet,
  batchSize,
  start,
  logger,
  process.exit
);

batchSocket.on('message', router);

batchSocket.bindSync(`tcp://*:${port}`);  
signalSocket.bindSync(`tcp://*:${pubPort}`);  
logger.info(`Server listening on port ${port}, signal publish on port ${pubPort}`);  

We make the arguments’ parsing very easy by using yargs again. The command must be invoked specifying a token as the only argument and must support several options:

  • port: used to specify in which port the batchSocket will be listening.
  • pubPort: used to specify which port will be used to publish the exit signal.
  • alphabet: a string containing all the characters in the alphabet we want to use to build all the possible strings used for the brute force.
  • batchSize: the size of every batch forwarded to the clients.
  • start: an index from the solution space from where to start the search (generally 0). Can be useful if you already analyzed part of the solution space.

In this case, we also add a check function to be sure that the JWT token we receive as an argument is well formatted and uses the HS256 algorithm for the signature.

In the rest of the code we initialize two ZeroMQ sockets: batchSocket and signalSocket - and we take them along with the token and the options received from the command line to initialize our router through the createRouter function that we wrote before.

Then we register the router listener to react to all the messages received on the batchSocket.

Finally, we bind our sockets to their respective ports to start to listen for incoming connections from the clients.

This completes our server application, and we are almost ready to give our little project a go. Hooray!

Logging utility

The last piece of code that we need is our little logger instance. We saw it being used in many of the modules we wrote before - so now let's code this missing piece.

As we briefly anticipated earlier, we are going to use winston for the logging functionality of this app.

We need a timestamp close to every log line to have an idea about how much time our application is taking to search for a solution - so we can write the following module to export a configured instance of winston that can simply import in every module and be ready to use:

// src/logger.js

'use strict';

const dateFormat = require('dateformat');  
const winston = require('winston');

module.exports = new (winston.Logger)({  
  transports: [
    new (winston.transports.Console)({
      timestamp: () => dateFormat(new Date(), 'yyyy-mm-dd HH:MM:ss'),
      colorize: true
    })
  ]
});

Notice, that we are just adding the timestamp with a specific format of our choice and then enabling the colorized output on the console.

Winston can be configured to support multiple transport layers like log files, network and syslog, so, if you want, you can get really fancy here and make it much more complex.

Running the application

We are finally ready to give our app a spin, let's brute force some JWT tokens!

Our token of choice is the following:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ  

This token is the default one from jwt.io and its password is secret.

To run the server, we need to launch the following command:

node src/server.js eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ  

This command starts the server and initializes it with the default alphabet (abcdefghijklmnopqrstuwxyzABCDEFGHIJKLMNOPQRSTUWXYZ0123456789). Considering that the password is long enough to keep our clients busy for a while and also that we already know the token password, we can cheat a little bit and specify a much smaller alphabet to speed up the search of the solution. If you feel like wanting to take a shortcut add the option -a cerst to the server start command!

Now you can run any number of clients in separate terminals with:

node src/client.js  

After the first client is connected, you will start to see the activity going on in both the server and the client terminals. It might take a while to discover the password - depending on the number of clients you run, the power of your local machine and the alphabet you choose to use.

In the following picture you can see an example of running both the server (left column) and four clients (right column) applications on the same machine:

ZeroMQ and Node.js: Example of executing a JWT token cracker in a single machine

In a real world case, you might want to run the server on a dedicated machine and then use as many machines as possible as clients. You could also run many clients per machine, depending on the number of cores in every machine.

Wrapping up

We are at the end of this experiment! I really hope you had fun and that you learned something new about Node.js, ZeroMQ and JWT tokens.

If you want to keep experimenting with this example and improve the application, here there are some ideas that you might want to work on:

Also, if you want to learn more about other Node.js design patterns (including more advanced topics like scalability, architectural, messaging and integration patterns) you can check my book Node.js Design Patterns - Second Edition:

Node.js design patterns book cover

A little challenge

Can you crack the following JWT token?

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJoaW50IjoiY2FuIHlvdSBjcmFjayBtZT8ifQ.a_8rViHX5q2oSZ3yB7H0lWniEYpLZrcgG8rJvkRTcoE  

If you can crack it there's a prize for you. Append the password you discovered to http://bit.ly/ (e.g., if the password is njdsp2e the resulting URL will be http://bit.ly/njdsp2e) to download the instructions to retrieve your prize! You won’t regret this challenge, I promise.

Have fun! Also, if you have questions or additional insights regarding this topic, please share them in the comments.

Acknowledgements

This article was peer reviewed with great care by Arthur Thevenet, Valerio De Carolis, Mario Casciaro, Padraig O'Brien, Joe Minichino and Andrea Mangano. Thank you guys for the amazing support!