Skip to content

Commit

Permalink
Add support of HS384 & HS512, password list and improved display (#34)
Browse files Browse the repository at this point in the history
* feat: add support for HS384 and HS512 algorithms
* feat: add dictionary option to use list of passwords
* refactor: expose chunk size from constants
* feat: check for child processes to finish, improve display, add new features
* docs: update docs with new parameters, bump version number
* feat: add conflict option of d & a + wrap to the terminal width
* Revert "feat: add conflict option of d & a + wrap to the terminal width"
This reverts commit 258fade.
* feat: use terminal width for yargs
  • Loading branch information
flibustier authored Nov 3, 2023
1 parent 859dc4e commit 604af56
Show file tree
Hide file tree
Showing 9 changed files with 197 additions and 91 deletions.
17 changes: 12 additions & 5 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,19 @@ npm install --global jwt-cracker
From command line:

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

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

Expand All @@ -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

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

Expand All @@ -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)
})
})
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>]'
'Usage: jwt-cracker -t <token> [-a <alphabet>] [--max <maxLength>] [-d <dictionaryFile>]'
)
.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,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
}

Expand All @@ -39,4 +45,8 @@ export default class ArgsParser {
get maxLength () {
return this.args.max
}

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
102 changes: 68 additions & 34 deletions index.js
100644 → 100755
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
}
}
Loading

0 comments on commit 604af56

Please sign in to comment.