Skip to content

Commit

Permalink
Merge branch 'master' into feat/force-skip-validation
Browse files Browse the repository at this point in the history
  • Loading branch information
maximeb97 committed Nov 7, 2023
2 parents 64f3a64 + 604af56 commit 19f2b49
Show file tree
Hide file tree
Showing 9 changed files with 199 additions and 92 deletions.
16 changes: 12 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -26,19 +26,20 @@ npm install --global jwt-cracker
From command line:

```bash
jwt-cracker -t <token> [-a <alphabet>] [--max <maxLength>] [-f]
jwt-cracker -t <token> [-a <alphabet>] [--max <maxLength>] [-d <dictionaryFilePath>] [-f]
```

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
* **force**: force script to execute when the token isn't valid

## Requirements

This script requires Node.js version 6.0.0 or higher
This script requires Node.js version 16.0.0 or higher

## Example

Expand All @@ -50,6 +51,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

Expand Down
16 changes: 13 additions & 3 deletions __tests__/index.test.js
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
60 changes: 40 additions & 20 deletions __tests__/jwtValidator.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,47 @@ 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'
const emptyToken = ''

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

Expand All @@ -55,29 +70,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)
})
})
Expand Down
14 changes: 12 additions & 2 deletions argsParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ export default class ArgsParser {
constructor () {
this.args = yargs(hideBin(process.argv))
.usage(
'Usage: jwt-cracker -t <token> [-a <alphabet>] [--max <maxLength>] [-f]'
'Usage: jwt-cracker -t <token> [-a <alphabet>] [--max <maxLength>] [-d <dictionaryFile>] [-f]'
)
.option('t', {
alias: 'token',
type: 'string',
describe: 'HS256 JWT token to crack',
describe: 'HMAC-SHA JWT token to crack',
demandOption: true
})
.option('a', {
Expand All @@ -24,12 +24,18 @@ 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'
})
.option('f', {
alias: 'force',
type: 'boolean',
describe: 'Skip token validation'
})
.help()
.wrap(yargs.terminalWidth)
.alias('h', 'help').argv
}

Expand All @@ -48,4 +54,8 @@ export default class ArgsParser {
get force () {
return this.args.force
}

get dictionaryFilePath () {
return this.args.dictionary
}
}
2 changes: 1 addition & 1 deletion constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export default class Constants {
return 12
}

static get MAX_CHUNK_SIZE () {
static get CHUNK_SIZE () {
return 20000
}

Expand Down
104 changes: 69 additions & 35 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,68 +1,100 @@
#!/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 force = args.force || false
const {
token,
alphabet,
maxLength,
dictionaryFilePath,
force
} = new ArgsParser()

const validToken = JWTValidator.validateToken(token)
const { isTokenValid, algorithm } = JWTValidator.validateToken(token)

if (!validToken && (!force || !token.length)) {
if (!isTokenValid && (!force || !token.length)) {
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
Expand All @@ -71,12 +103,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)
}
}
Loading

0 comments on commit 19f2b49

Please sign in to comment.