diff --git a/README.md b/README.md index f782075..caec524 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ # jwt-cracker -Simple HS256 JWT token brute force cracker. +Simple HS256, HS384 & HS512 JWT token brute force cracker. Effective only to crack JWT tokens with weak secrets. **Recommendation**: Use strong long secrets or RS256 tokens. @@ -26,19 +26,19 @@ npm install --global jwt-cracker From command line: ```bash -jwt-cracker -t [-a ] [--max ] +jwt-cracker -t [-a ] [--max ] [-d ] ``` Where: -* **token**: the full HS256 JWT token string to crack +* **token**: the full HS256-512 JWT token string to crack * **alphabet**: the alphabet to use for the brute force (default: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") * **maxLength**: the max length of the string generated during the brute force (default: 12) - +* **dictionaryFilePath**: path to a list of passwords (one per line) to use instead of brute force ## Requirements -This script requires Node.js version 6.0.0 or higher +This script requires Node.js version 16.0.0 or higher ## Example @@ -50,6 +50,13 @@ jwt-cracker -t eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwi It takes about 2 hours in a Macbook Pro (2.5GHz quad-core Intel Core i7). +Or using a list of passwords taken from https://github.com/danielmiessler/SecLists + +```bash +jwt-cracker -t eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ -d darkweb2017-top10000.txt +``` + +It takes less than a second. ## Contributing diff --git a/__tests__/index.test.js b/__tests__/index.test.js index 5b28d06..b3c171d 100644 --- a/__tests__/index.test.js +++ b/__tests__/index.test.js @@ -1,11 +1,21 @@ import { describe, expect, test } from '@jest/globals' import { spawn } from 'node:child_process' -const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Imp3dC1jcmFjbGVyIiwiaWF0IjoxNTE2MjM5MDIyfQ.29OQn8UytvagAsG-OwnkzxO2lBw8QEWOuc8ltSZRWCU' +const tokenHS256 = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Imp3dC1jcmFjbGVyIiwiaWF0IjoxNTE2MjM5MDIyfQ.29OQn8UytvagAsG-OwnkzxO2lBw8QEWOuc8ltSZRWCU' +const tokenHS512 = 'eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSm9obiJ9.tR6snQQ6RIf0RH9oEl_v5xDlLLduU2gzZhD86QO64ZtXv30Vjcpi61vbB7kBMFAvZFozGrtdhlonAzQ-k9OuZA' describe('Jwt-cracker', () => { - test('should return secret found', (done) => { - const app = spawn('node', ['index.js', '-t', token]) + test('should return secret found with HS256', (done) => { + const app = spawn('node', ['index.js', '-t', tokenHS256]) + + app.on('exit', (code) => { + expect(code).toBe(0) + done() + }) + }, 15000) // 15 Seconds timeout + + test('should return secret found with HS512', (done) => { + const app = spawn('node', ['index.js', '-t', tokenHS512]) app.on('exit', (code) => { expect(code).toBe(0) diff --git a/__tests__/jwtValidator.test.js b/__tests__/jwtValidator.test.js index d0fb9c1..4fab072 100644 --- a/__tests__/jwtValidator.test.js +++ b/__tests__/jwtValidator.test.js @@ -4,31 +4,46 @@ import { describe, expect, test } from '@jest/globals' describe('JWTValidator', () => { const validHS256Token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Imp3dC1jcmFja2VyIn0.TaRgJUlx6BXwhna8AYF8xGyAMmxODXYIjnNuYju--c8' + const validHS384Token = 'eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSm9obiJ9.zJjZgooLqpGti_j6-KRgY-22xWlExFDhRLho0EzRY6iAk68tu-czZOp13AeJ6aHo' + const validHS512Token = 'eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSm9obiJ9.tR6snQQ6RIf0RH9oEl_v5xDlLLduU2gzZhD86QO64ZtXv30Vjcpi61vbB7kBMFAvZFozGrtdhlonAzQ-k9OuZA' const invalidFormatToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Imp3dC1jcmFja2VyIn0' const invalidFormatEmptyPartsToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..' const invalidHeaderToken = 'eyJhJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ikpqd3QtY3JhY2tlciJ9.c5ZqtVGS-Jc6WUJsaRBVzfpUOcMFLu0lo0fd2FwDnJE' const nonJwtTypToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6Ik5vdC1Kd3QifQ.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ikpqd3QtY3JhY2tlciJ9.8SmsCZptHRoDeGclg5Tl_N5-tSJF24BBPYa_YKp8b4g' - const validButUnsupportedHS512Token = 'eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Imp3dC1jcmFja2VyIn0.CcyaiMxfTVbG0SNPW9btRr5mJ3DCt0LOjVFtNJZW6ogjJxbeT6tAixi1uut2M8rlbTBYOqAxD56eIL7AXXaatw' + const validButUnsupportedRS256Token = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJSUzI1NmluT1RBIiwibmFtZSI6IkpvaG4gRG9lIn0.ICV6gy7CDKPHMGJxV80nDZ7Vxe0ciqyzXD_Hr4mTDrdTyi6fNleYAyhEZq2J29HSI5bhWnJyOBzg2bssBUKMYlC2Sr8WFUas5MAKIr2Uh_tZHDsrCxggQuaHpF4aGCFZ1Qc0rrDXvKLuk1Kzrfw1bQbqH6xTmg2kWQuSGuTlbTbDhyhRfu1WDs-Ju9XnZV-FBRgHJDdTARq1b4kuONgBP430wJmJ6s9yl3POkHIdgV-Bwlo6aZluophoo5XWPEHQIpCCgDm3-kTN_uIZMOHs2KRdb6Px-VN19A5BYDXlUBFOo-GvkCBZCgmGGTlHF_cWlDnoA9XTWWcIYNyUI4PXNw' describe('validateToken', () => { test('should return true for a valid HS256 JWT token', () => { - const result = JWTValidator.validateToken(validHS256Token) - expect(result).toBe(true) + const { isTokenValid, algorithm } = JWTValidator.validateToken(validHS256Token) + expect(isTokenValid).toBe(true) + expect(algorithm).toBe('HS256') + }) + + test('should return true for a valid HS384 JWT token', () => { + const { isTokenValid, algorithm } = JWTValidator.validateToken(validHS384Token) + expect(isTokenValid).toBe(true) + expect(algorithm).toBe('HS384') + }) + + test('should return true for a valid HS512 JWT token', () => { + const { isTokenValid, algorithm } = JWTValidator.validateToken(validHS512Token) + expect(isTokenValid).toBe(true) + expect(algorithm).toBe('HS512') }) test('should return false for a token with less than three parts', () => { - const result = JWTValidator.validateToken(invalidFormatToken) - expect(result).toBe(false) + const { isTokenValid } = JWTValidator.validateToken(invalidFormatToken) + expect(isTokenValid).toBe(false) }) test('should return false for an unsupported token typ', () => { - const result = JWTValidator.validateToken(nonJwtTypToken) - expect(result).toBe(false) + const { isTokenValid } = JWTValidator.validateToken(nonJwtTypToken) + expect(isTokenValid).toBe(false) }) - test('should return false for an unsupported HS512 algorithm', () => { - const result = JWTValidator.validateToken(validButUnsupportedHS512Token) - expect(result).toBe(false) + test('should return false for an unsupported token algorithm', () => { + const { isTokenValid } = JWTValidator.validateToken(validButUnsupportedRS256Token) + expect(isTokenValid).toBe(false) }) }) @@ -49,29 +64,34 @@ describe('JWTValidator', () => { }) }) - describe('validateHS256AlgorithmHeader', () => { + describe('validateHmacAlgorithmHeader', () => { test('should return true for valid token with typ JWT and algorithm HS256', () => { - const result = JWTValidator.validateToken(validHS256Token) - expect(result).toBe(true) + const { isTokenValid } = JWTValidator.validateToken(validHS256Token) + expect(isTokenValid).toBe(true) }) - test('should return false for a token with a invalid number of parts', () => { - const result = JWTValidator.validateHS256AlgorithmHeader(invalidFormatToken) - expect(result).toBe(false) + test('should return true for valid token with typ JWT and algorithm HS384', () => { + const { isTokenValid } = JWTValidator.validateToken(validHS384Token) + expect(isTokenValid).toBe(true) + }) + + test('should return true for valid token with typ JWT and algorithm HS512', () => { + const { isTokenValid } = JWTValidator.validateToken(validHS512Token) + expect(isTokenValid).toBe(true) }) test('should return false for a token with a invalid header', () => { - const result = JWTValidator.validateHS256AlgorithmHeader(invalidHeaderToken) + const result = JWTValidator.validateHmacAlgorithmHeader(invalidHeaderToken) expect(result).toBe(false) }) test('should return false for an unsupported token typ', () => { - const result = JWTValidator.validateHS256AlgorithmHeader(nonJwtTypToken) + const result = JWTValidator.validateHmacAlgorithmHeader(nonJwtTypToken) expect(result).toBe(false) }) - test('should return false for an unsupported HS512 algorithm', () => { - const result = JWTValidator.validateHS256AlgorithmHeader(validButUnsupportedHS512Token) + test('should return false for an unsupported token algorithm', () => { + const result = JWTValidator.validateHmacAlgorithmHeader(validButUnsupportedRS256Token) expect(result).toBe(false) }) }) diff --git a/argsParser.js b/argsParser.js index b37a60d..9529773 100644 --- a/argsParser.js +++ b/argsParser.js @@ -6,12 +6,12 @@ export default class ArgsParser { constructor () { this.args = yargs(hideBin(process.argv)) .usage( - 'Usage: jwt-cracker -t [-a ] [--max ]' + 'Usage: jwt-cracker -t [-a ] [--max ] [-d ]' ) .option('t', { alias: 'token', type: 'string', - describe: 'HS256 JWT token to crack', + describe: 'HMAC-SHA JWT token to crack', demandOption: true }) .option('a', { @@ -24,7 +24,13 @@ export default class ArgsParser { describe: 'Maximum length of the secret', default: Constants.DEFAULT_MAX_SECRET_LENGTH }) + .option('d', { + alias: 'dictionary', + type: 'string', + describe: 'Password file to use instead of the brute force' + }) .help() + .wrap(yargs.terminalWidth) .alias('h', 'help').argv } @@ -39,4 +45,8 @@ export default class ArgsParser { get maxLength () { return this.args.max } + + get dictionaryFilePath () { + return this.args.dictionary + } } diff --git a/constants.js b/constants.js index 7dcb211..a84f4f8 100644 --- a/constants.js +++ b/constants.js @@ -7,7 +7,7 @@ export default class Constants { return 12 } - static get MAX_CHUNK_SIZE () { + static get CHUNK_SIZE () { return 20000 } diff --git a/index.js b/index.js old mode 100644 new mode 100755 index d46a9aa..34b9548 --- a/index.js +++ b/index.js @@ -1,67 +1,99 @@ #!/usr/bin/env node -import { fileURLToPath } from 'node:url' import { join } from 'node:path' import { fork } from 'node:child_process' +import { fileURLToPath } from 'node:url' +import { createReadStream } from 'node:fs' +import { createInterface } from 'node:readline' + import variationsStream from 'variations-stream' + +import Constants from './constants.js' import ArgsParser from './argsParser.js' import JWTValidator from './jwtValidator.js' -import Constants from './constants.js' - -const __dirname = fileURLToPath(new URL('.', - import.meta.url)) -const args = new ArgsParser() +const __dirname = fileURLToPath(new URL('.', import.meta.url)) +const numberFormatter = Intl.NumberFormat('en', { notation: 'compact' }).format -const token = args.token -const alphabet = args.alphabet -const maxLength = args.maxLength +const { + token, + alphabet, + maxLength, + dictionaryFilePath +} = new ArgsParser() -const validToken = JWTValidator.validateToken(token) +const { isTokenValid, algorithm } = JWTValidator.validateToken(token) -if (!validToken) { +if (!isTokenValid) { process.exit(Constants.EXIT_CODE_FAILURE) } +const timeTaken = (startTime) => (new Date().getTime() - startTime) / 1000 + const printResult = function (startTime, attempts, result) { if (result) { console.log('SECRET FOUND:', result) } else { console.log('SECRET NOT FOUND') } - console.log('Time taken (sec):', (new Date().getTime() - startTime) / 1000) - console.log('Attempts:', attempts) + console.log('Time taken (sec):', timeTaken(startTime)) + console.log('Total attempts:', attempts) } const [header, payload, signature] = token.split('.') const content = `${header}.${payload}` -const startTime = new Date().getTime() -let attempts = 0 -const chunkSize = 20000 let chunk = [] +let attempts = 0 +let isStreamClosed = false +const startTime = new Date().getTime() +const childProcesses = [] -variationsStream(alphabet, maxLength) - .on('data', function (comb) { - chunk.push(comb) - if (chunk.length >= chunkSize) { - // save chunk and reset it - forkChunk(chunk) - chunk = [] - } +if (dictionaryFilePath) { + const lineReader = createInterface({ + input: createReadStream(dictionaryFilePath) }) - .on('end', function () { - printResult(startTime, attempts) + + lineReader.on('error', function () { + console.log(`Unable to read the dictionary file "${dictionaryFilePath}" (make sure the file path exists)`) process.exit(Constants.EXIT_CODE_FAILURE) }) + lineReader.on('line', addToQueue) + lineReader.on('close', closeStream) +} else { + variationsStream(alphabet, maxLength) + .on('data', addToQueue) + .on('end', closeStream) +} + +function closeStream () { + // purge remaining items in chunk + purgeQueue() + isStreamClosed = true +} + +function purgeQueue () { + // save chunk and reset it + forkChunk(chunk) + chunk = [] +} + +function addToQueue (comb) { + chunk.push(comb) + if (chunk.length >= Constants.CHUNK_SIZE) { + purgeQueue() + } +} function forkChunk (chunk) { const child = fork(join(__dirname, 'process-chunk.js')) - child.send({ chunk, content, signature }) + childProcesses.push(child) + child.send({ chunk, content, signature, algorithm }) child.on('message', function (result) { - attempts += chunkSize - if (result === null && attempts % 100000 === 0) { - console.log('Attempts:', attempts) + attempts += chunk.length + if (result === null && attempts % (Constants.CHUNK_SIZE * 5) === 0) { + const speed = numberFormatter(Math.trunc(attempts / timeTaken(startTime))) + console.log(`Attempts: ${attempts} (${speed}/s last attempt was '${chunk[chunk.length - 1]}')`) } if (result) { // secret found, print result and exit @@ -70,12 +102,14 @@ function forkChunk (chunk) { } }) - child.on('exit', function () { - // check if all child processes have finished, and if so, exit - checkFinished() - }) + child.on('exit', checkFinished) } function checkFinished () { // check if all child processes have finished, and if so, exit + childProcesses.pop() + if (isStreamClosed && childProcesses.length === 0) { + printResult(startTime, attempts) + process.exit(Constants.EXIT_CODE_FAILURE) + } } diff --git a/jwtValidator.js b/jwtValidator.js index 7bac9b6..462ade7 100644 --- a/jwtValidator.js +++ b/jwtValidator.js @@ -1,35 +1,49 @@ export default class JWTValidator { - static validateToken (token) { - return ( - this.validateGeneralJwtFormat(token) && - this.validateHS256AlgorithmHeader(token) - ) - } + static SUPPORTED_ALGORITHM = [ + 'HS256', + 'HS384', + 'HS512' + ] - static validateGeneralJwtFormat (token) { + static decodeHeader (token) { const parts = token.split('.') - if (parts.length !== 3 || !parts.every(part => part.length > 0)) { - console.log('Invalid token format') - return false + try { + const decodedHeader = JSON.parse(Buffer.from(parts[0], 'base64').toString('utf-8')) + return decodedHeader + } catch (e) { + console.log('Invalid token format. Invalid header.') + return null } + } - return true + static validateToken (token) { + const isTokenValid = this.validateGeneralJwtFormat(token) && this.validateHmacAlgorithmHeader(token) + const algorithm = isTokenValid ? this.decodeHeader(token).alg : '' + + return { isTokenValid, algorithm } } - static validateHS256AlgorithmHeader (token) { + static validateGeneralJwtFormat (token) { const parts = token.split('.') - let decodedHeader if (parts.length !== 3) { console.log('Invalid token format. Invalid number of parts.') return false } - try { - decodedHeader = JSON.parse(Buffer.from(parts[0], 'base64').toString('utf-8')) - } catch (e) { - console.log('Invalid token format. Invalid header.') + if (!parts.every(part => part.length > 0)) { + console.log('Invalid token format. Parts should not be empty.') + return false + } + + return true + } + + static validateHmacAlgorithmHeader (token) { + const decodedHeader = this.decodeHeader(token) + + if (!decodedHeader) { return false } @@ -38,8 +52,8 @@ export default class JWTValidator { return false } - if (decodedHeader.alg !== 'HS256') { - console.log(`Unsupported algorithm: ${decodedHeader.alg}`) + if (!this.SUPPORTED_ALGORITHM.includes(decodedHeader.alg)) { + console.log(`Unsupported algorithm: ${decodedHeader.alg}. Only ${this.SUPPORTED_ALGORITHM.join(', ')} are supported.`) return false } diff --git a/package.json b/package.json index 08d1e2c..4c4a955 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "jwt-cracker", - "version": "3.0.0", - "description": "Simple HS256 JWT token brute force cracker", + "version": "4.0.0", + "description": "Simple HS256-512 JWT token brute force cracker", "main": "index.js", "type": "module", "bin": { @@ -42,6 +42,10 @@ { "name": "Rob Waller", "url": "https://github.com/RobDWaller" + }, + { + "name": "Flibustier", + "url": "https://github.com/flibustier" } ], "repository": { diff --git a/process-chunk.js b/process-chunk.js index 9008dcf..a400313 100644 --- a/process-chunk.js +++ b/process-chunk.js @@ -1,16 +1,23 @@ import { createHmac } from 'node:crypto' -const generateSignature = function (content, secret) { - return createHmac('sha256', secret) +const DIGEST_ALGORITHM = { + HS256: 'sha256', + HS384: 'sha384', + HS512: 'sha512' +} + +const signatureGenerator = (algorithm, content) => (secret) => + createHmac(DIGEST_ALGORITHM[algorithm], secret) .update(content) .digest('base64') .replace(/=/g, '') .replace(/\+/g, '-') .replace(/\//g, '_') -} -process.on('message', function ({ chunk, content, signature }) { + +process.on('message', function ({ chunk, content, signature, algorithm }) { + const generateSignature = signatureGenerator(algorithm, content) for (let i = 0; i < chunk.length; i++) { - const currentSignature = generateSignature(content, chunk[i]) + const currentSignature = generateSignature(chunk[i]) if (currentSignature === signature) { process.send(chunk[i]) process.exit(0)