jwtear 0.2.0 → 1.0.0.pre

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,8 +8,8 @@ Gem::Specification.new do |spec|
8
8
  spec.authors = ["KING SABRI"]
9
9
  spec.email = ["king.sabri@gmail.com"]
10
10
 
11
- spec.summary = %q{JWTear, command-line tool and library to parse, create and manipulate JWT tokens for security testing purposes.}
12
- spec.description = %q{JWTear, command-line tool and library to parse, create and manipulate JWT tokens for security testing purposes.}
11
+ spec.summary = %q{JWTear, a modular command-line tool to parse, create and manipulate JWT tokens for security testing purposes.}
12
+ spec.description = %q{JWTear, a modular command-line tool to parse, create and manipulate JWT tokens for security testing purposes.}
13
13
  spec.homepage = "https://github.com/KINGSABRI/jwtear"
14
14
  spec.license = "MIT"
15
15
 
@@ -18,5 +18,11 @@ Gem::Specification.new do |spec|
18
18
  spec.executables = ['jwtear']
19
19
  spec.require_paths = ["lib"]
20
20
 
21
- # spec.add_dependency ''
21
+ spec.add_dependency 'gli', '~> 2.19', '>= 2.19.0'
22
+ spec.add_dependency 'json-jwt', '~> 1.10', '>= 1.10.2'
23
+ spec.add_dependency 'jwe', "~> 0.4.0"
24
+ spec.add_dependency 'tty-markdown', "~> 0.6.0"
25
+ spec.add_dependency 'tty-pager', "~> 0.12.1"
26
+
27
+ # spec.add_development_dependency('rake', '~> 0.9.2.2')
22
28
  end
@@ -1,25 +1,21 @@
1
- # Standard libraries
2
- require 'base64'
3
- require 'json'
4
- require 'digest'
5
- require 'openssl'
6
- require 'open-uri'
7
-
8
1
  # JWTear
9
2
  require_relative 'jwtear/version'
10
3
  require_relative 'jwtear/errors'
11
- require_relative 'jwtear/extensions'
12
- require_relative 'jwtear/jwt'
13
- require_relative 'jwtear/utils'
14
-
4
+ require_relative 'jwtear/helpers/utils'
5
+ require_relative 'jwtear/helpers/extensions'
6
+ require_relative 'jwtear/token'
15
7
 
16
- module JWTear
17
- include JWTear::Utils
8
+ # External gems
9
+ require 'json/jwt'
10
+ require 'tty-markdown'
11
+ require 'tty-pager'
12
+ require 'colorize'
18
13
 
19
- String.class_eval do
20
- include Extensions::Core::String
21
- end
22
- # NilClass.class_eval do
23
- # include Extensions::Core::NilClass
24
- # end
14
+ module JWTear
15
+ # extend JWTear::Helpers::Extensions::Print
16
+ # include JWTear::Helpers::Extensions::Print
17
+ # Kernel.include JWTear::Helpers::Extensions::Print
18
+ # autoload(:Print, 'jwtear/helpers/extensions')
25
19
  end
20
+
21
+
@@ -1,9 +1,16 @@
1
1
  module JWTear
2
2
 
3
3
  # Token
4
- class InvalidTokenError < Exception; end
4
+ # class InvalidTokenError < JSON::JWT::InvalidFormat; end
5
5
 
6
6
  # Algorithm Errors
7
- class AlgorithmRequiresKeyError < TypeError; end
8
- class AlgorithmUnknownError < Exception; end
9
- end
7
+ # class AlgorithmRequiresKeyError < JSON::JWT::TypeError; end
8
+ # class AlgorithmUnknownError < JSON::JWS::UnexpectedAlgorithm; end
9
+ # class InvalidFormat < JSON::JWT::InvalidFormat; end
10
+ # class Error < StandardError; end
11
+
12
+ # class MissingOption < CustomExit
13
+ #
14
+ # end
15
+
16
+ end
@@ -0,0 +1,17 @@
1
+ module JWTear
2
+ module Helpers
3
+ module Extensions
4
+ # Add print styles
5
+ module Print
6
+ def print_status(msg); puts "[*] ".blue + "#{msg}"; end
7
+ def print_good(msg); puts "[+] ".green + "#{msg}"; end
8
+ def print_error(msg); puts "[x] ".red + "#{msg}"; end
9
+ def print_bad(msg); puts "[!] ".red + "#{msg}"; end
10
+ def print_warning(msg); puts "[!] ".yellow + "#{msg}"; end
11
+ def print_h1(msg); puts "-[" + "#{msg}".green.bold + "]----"; end
12
+ def print_h2(msg); puts "[+] ".green + "#{msg}:" ; end
13
+ def print_h3(*msg); print " - ".cyan.bold + "#{msg[0]}:" " #{msg[1..-1].join(' ')}\n" ; end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,71 @@
1
+ module JWTear
2
+ module Helpers
3
+ module Utils
4
+
5
+ # Check latest version
6
+ def latest_version
7
+ begin
8
+ current_version = JWTear::VERSION
9
+ rubygem_api = JSON.parse open("https://rubygems.org/api/v1/versions/jwtear.json").read
10
+ remote_version = rubygem_api.first["number"]
11
+ latest = remote_version.eql?(current_version)? true : false
12
+
13
+ latest ? current_version : remote_version
14
+ rescue Exception => e
15
+ print_bad " Couldn't check the latest version, please check internet connectivity."
16
+ exit!
17
+ end
18
+ end
19
+
20
+ # read key as a string or from file(eg. pub_key.pem)
21
+ def read_key(key)
22
+ if key
23
+ File.file?(key)? File.read(key) : key
24
+ end
25
+ end
26
+
27
+ # check_dependencies
28
+ # check dependencies for plugins and throw a gentle error if not installed
29
+ # @param deps [Hash]
30
+ # The key is the library to be require, the key is the gem to be required
31
+ # @example
32
+ # deps = {'async-io' => 'async/ip'}
33
+ # check_dependencies(deps)
34
+ #
35
+ def check_dependencies(deps={})
36
+ return if deps.empty?
37
+ missing = []
38
+
39
+ deps.each do |gem, req|
40
+ begin
41
+ require req
42
+ rescue LoadError
43
+ missing << gem
44
+ end
45
+ end
46
+ ensure
47
+ unless missing.empty?
48
+ print_error "Missing dependencies!"
49
+ print_warning "Please install as follows:"
50
+ puts "gem install #{missing.join(' ')}"
51
+ exit!
52
+ end
53
+ end
54
+
55
+ # JWTear's logo
56
+ def banner
57
+ %Q{\n 888888 888 888 88888888888
58
+ "88b 888 o 888 888
59
+ 888 888 d8b 888 888
60
+ 888 888 d888b 888 888 .d88b. 8888b. 888d888
61
+ 888 888d88888b888 888 d8P Y8b "88b 888P"
62
+ 888 88888P Y88888 888 88888888 .d888888 888
63
+ 88P 8888P Y8888 888 Y8b. 888 888 888
64
+ 888 888P Y888 888 "Y8888 "Y888888 888
65
+ .d88P v#{JWTear::VERSION}
66
+ .d88P"
67
+ 888P" }
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,102 @@
1
+ require 'jwe'
2
+
3
+ module JWTear
4
+ # JWE
5
+ # Takes a parsed token from JSON::JWT#decode
6
+ #
7
+ class JWE
8
+ include JWTear::Helpers::Extensions::Print
9
+
10
+ attr_accessor :header, :encrypted_key, :iv, :cipher_text, :authentication_tag,
11
+ :kid, :auth_data, :plaintext, :alg, :enc, :zip, :cek,
12
+ :iss, :sub, :iat
13
+
14
+ # parse
15
+ # is a basic parser for JWE token
16
+ #
17
+ # @param token [String]
18
+ # parsed token string
19
+ #
20
+ # @return [Self]
21
+ #
22
+ def parse(token)
23
+ jwt = JSON::JWT.decode(token, :skip_decryption, :skip_verification)
24
+ @header = jwt.header
25
+ @encrypted_key = jwt.send(:jwe_encrypted_key)
26
+ @iv = jwt.iv
27
+ @cipher_text = jwt.cipher_text
28
+ @authentication_tag = jwt.send(:authentication_tag)
29
+ @algorithm = jwt.algorithm
30
+ @auth_data = jwt.auth_data
31
+ @plaintext = jwt.send(:plain_text)
32
+ @kid = jwt.kid
33
+ @alg = @header["alg"]
34
+ @typ = @header["typ"]
35
+ @cty = @header["cty"]
36
+ @enc = @header["enc"]
37
+ @zip = @header["zip"]
38
+ @iat = @encrypted_key["iat"]
39
+ @iss = @encrypted_key["iss"]
40
+ @cek = @encrypted_key
41
+ self
42
+ rescue JSON::JWS::UnexpectedAlgorithm => e
43
+ puts e.full_message
44
+ rescue JSON::JWT::InvalidFormat => e
45
+ print_error e.message
46
+ puts token
47
+ exit!
48
+ end
49
+
50
+ def to_json_presentation
51
+ header = @header
52
+ if is_encrypted?(@encrypted_key)
53
+ encrypted_key = Base64.urlsafe_encode64(@encrypted_key, padding: false)
54
+ else
55
+ encrypted_key = @encrypted_key.to_json
56
+ end
57
+ iv = Base64.urlsafe_encode64(@iv)
58
+ cipher_text = Base64.urlsafe_encode64(@cipher_text, padding: false)
59
+ authentication_tag = Base64.urlsafe_encode64(@authentication_tag, padding: false)
60
+
61
+ "#{header.to_json}" + "●" +
62
+ "#{encrypted_key}" + "●" +
63
+ "#{iv}" + "●" +
64
+ "#{cipher_text}" + "●" +
65
+ "#{authentication_tag}"
66
+ end
67
+
68
+ # generate_jwe
69
+ # generate JWE token
70
+ #
71
+ # @param header [JSON]
72
+ # @param payload [JSON]
73
+ # @param key [String]
74
+ #
75
+ # @return [String] the generated token
76
+ #
77
+ def generate_jwe(header:, payload:, key:)
78
+ key = OpenSSL::PKey::RSA.new(key)
79
+ jwt = JSON::JWT.new(JSON.parse(payload, symbolize_names: true))
80
+ jwt.header = JSON.parse(header, symbolize_names: true)
81
+ ::JWE.encrypt(payload, key, enc: jwt.header[:enc]) # I had to use this gem as json-jwt does not support A192GCM AFAIK
82
+ rescue TypeError => e
83
+ print_bad "Invalid data type."
84
+ print_warning "Make sure your public/private key file exists."
85
+ rescue ArgumentError => e
86
+ print_error e.message
87
+ print_warning "Make sure that you have a proper header."
88
+ puts jwt.header
89
+ rescue OpenSSL::PKey::RSAError => e
90
+ print_error "#{e.message} '#{key}'"
91
+ print_warning "Make sure your public/private key file exists."
92
+ exit!
93
+ end
94
+
95
+ def is_encrypted?(item)
96
+ JSON.parse item
97
+ false
98
+ rescue JSON::ParserError
99
+ true
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,80 @@
1
+ module JWTear
2
+ # JWS
3
+ # Takes a parsed token from JSON::JWT#decode
4
+ #
5
+ class JWS
6
+ include JWTear::Helpers::Extensions::Print
7
+
8
+ attr_accessor :header, :payload, :signature,
9
+ :typ, :cty, :alg, :kid, :jku,
10
+ :iss, :sub, :iat, :exp, :aud, :hd,
11
+ :azp, :email, :at_hash, :email_verified
12
+
13
+ # parse
14
+ # is a basic parser for JWS token
15
+ #
16
+ # @param token [String]
17
+ # parsed token string
18
+ #
19
+ # @return [Self]
20
+ #
21
+ def parse(token)
22
+ jwt = JSON::JWT.decode(token, :skip_verification)
23
+ @header = jwt.header
24
+ @payload = jwt.to_h
25
+ @signature = jwt.signature
26
+ @alg = @header["alg"]
27
+ @typ = @header["typ"]
28
+ @cty = @header["cty"]
29
+ @kid = @header["kid"]
30
+ @jku = @header["jku"]
31
+ @iat = @payload["iat"]
32
+ self
33
+ rescue JSON::JWT::InvalidFormat => e
34
+ print_error e.message
35
+ puts token
36
+ exit!
37
+ rescue Exception => e
38
+ puts e.full_message
39
+ end
40
+
41
+ def to_json_presentation
42
+ "#{@header.to_json}" + "●" + "#{@payload.to_json}" + "●" + "#{Base64.urlsafe_encode64(@signature, padding: false)}"
43
+ end
44
+
45
+ # generate_jws
46
+ # generate JWS token
47
+ #
48
+ # @param header [JSON]
49
+ # @param payload [JSON]
50
+ # @param key [String]
51
+ #
52
+ # @return [String] the generated token
53
+ #
54
+ def generate_jws(header:, payload:, key:)
55
+ jwt = JSON::JWT.new(JSON.parse(payload, symbolize_names: true))
56
+ jwt.header = JSON.parse(header, symbolize_names: true)
57
+ handle_signing(jwt, key)
58
+ rescue JSON::JWS::UnexpectedAlgorithm => e
59
+ puts "Unexpected algorithm '#{jwt.header[:alg]}'."
60
+ puts e.message
61
+ exit!
62
+ end
63
+
64
+ private
65
+
66
+ # handle_signing
67
+ # Handles the algorithm 'none'.
68
+ # @param jwt [JSON]
69
+ # @param key [String]
70
+ #
71
+ def handle_signing(jwt, key=nil)
72
+ if jwt.alg =~ /none/i
73
+ jwt.to_s
74
+ else
75
+ raise JSON::JWS::UnexpectedAlgorithm.new("Encryption algorithm '#{jwt.alg}' requires key.") if key.nil?
76
+ jwt.sign(key).to_s
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,67 @@
1
+ require_relative 'jws'
2
+ require_relative 'jwe'
3
+
4
+ module JWTear
5
+ class Token
6
+ include JWTear::Helpers::Extensions::Print
7
+
8
+ def initialize
9
+ @jws = JWS.new
10
+ @jwe = JWE.new
11
+ end
12
+
13
+ # parse
14
+ # An interface for JWS and JWE parse operation.
15
+ # @param token [String]
16
+ #
17
+ # @return [JWS|JWE]
18
+ def parse(token)
19
+ token_segments = token.split('.').size
20
+ if token_segments <= 3 # JWS
21
+ @jws.parse(token)
22
+ else # JWE
23
+ @jwe.parse(token)
24
+ end
25
+ rescue Exception => e
26
+ print_error "Unknown Exception: #{method(__method__).owner}"
27
+ print_warning 'Please report the issue to: https://github.com/KINGSABRI/jwtear/issues'.underline
28
+ puts e
29
+ puts e.backtrace
30
+ exit!
31
+ end
32
+
33
+ # generate
34
+ # An interface for JWS and JWE token generation operation.
35
+ # @param type [Symbol]
36
+ # @param header [JSON]
37
+ # @param payload [JSON]
38
+ # @param key [String]
39
+ #
40
+ # @example
41
+ # token = JWTear::Token.new
42
+ # token.generate(:jws, header: '{"alg":"HS256","typ":"JWT"}', payload: '{"user":"admin"}', key: "P@ssw0rd123")
43
+ #
44
+ # @return [JWS | JWE]
45
+ def generate(type, header:, payload:, key:)
46
+ case type
47
+ when :jws
48
+ @jws.generate_jws(header:header , payload:payload , key:key)
49
+ when :jwe
50
+ @jwe.generate_jwe(header:header , payload:payload , key:key)
51
+ else
52
+ print_error "Unknown type: #{type}"
53
+ raise
54
+ end
55
+ rescue JSON::ParserError => e
56
+ print_error "Unexpected Token."
57
+ puts e.message
58
+ rescue Exception => e
59
+ method = method(__method__)
60
+ print_error "Unknown Exception: #{method.owner}##{method.name}"
61
+ print_warning 'Please report the issue to: https://github.com/KINGSABRI/jwtear/issues'.underline
62
+ puts e.full_message
63
+ exit!
64
+ end
65
+ end
66
+ end
67
+
@@ -1,3 +1,3 @@
1
1
  module JWTear
2
- VERSION = "0.2.0"
2
+ VERSION = "1.0.0.pre"
3
3
  end
@@ -0,0 +1,103 @@
1
+ module JWTear
2
+ module CLI
3
+ extend GLI::App
4
+ extend JWTear::Helpers::Extensions::Print
5
+
6
+ desc %Q{plugin to offline bruteforce and crack token's signature.}
7
+ command [:bruteforce, :bfs] do |c|
8
+ c.desc 'Token to crack its key.'
9
+ c.arg_name 'JWT_TOKEN'
10
+ c.flag [:t, :token], required: true
11
+
12
+ c.desc 'Password or password list to bruteforce the signature'
13
+ c.arg_name 'PASS_LIST'
14
+ c.flag [:l, :p, :list, :password], required: true
15
+
16
+ c.desc "Run verbosely."
17
+ c.switch [:v, :verbose], negatable: false
18
+
19
+ c.example %Q{jwtear bruteforce -t TOKEN -l rockyou.list -v}
20
+ c.example %Q{jwtear bruteforce -t TOKEN -l P@ssw0rd123}
21
+
22
+ c.action do |_, options, _|
23
+ begin
24
+ bf = Bruteforce.new(options[:token], options[:list])
25
+ bf.run(options[:verbose])
26
+ end
27
+ end
28
+
29
+ end
30
+ end
31
+
32
+ class Bruteforce
33
+ include JWTear::Helpers::Extensions::Print
34
+ include JWTear::Helpers::Utils
35
+
36
+ def initialize(token, list)
37
+ deps = {'async-io' => 'async/io'}
38
+ check_dependencies(deps)
39
+ @token = Token.new
40
+ @jws = @token.parse(token)
41
+ @list = list
42
+ end
43
+
44
+ def run(verbose=false)
45
+ keys = handle_key
46
+ case
47
+ when keys.kind_of?(Enumerator::Lazy)
48
+ keys.each do |key|
49
+ print_status "Trying password: #{key}" if verbose
50
+
51
+ gen_token = @token.generate(:jws, header: @jws.header.to_json, payload:@jws.payload.to_json , key: key)
52
+ sig = gen_token.split('.').last
53
+ if sig == Base64.urlsafe_encode64(@jws.signature, padding: false)
54
+ print_good "Password found: #{key}"
55
+ puts gen_token
56
+ exit!
57
+ else
58
+ print_bad "Invalid key: #{key}" if verbose
59
+ # puts gen_token if verbose
60
+ end
61
+ end
62
+ when keys.kind_of?(String)
63
+ gen_token = @token.generate(:jws, header: @jws.header.to_json, payload:@jws.payload.to_json , key: keys)
64
+ sig = gen_token.split('.').last
65
+ if sig == Base64.urlsafe_encode64(@jws.signature, padding: false)
66
+ print_good "Password found: #{keys}"
67
+ puts gen_token
68
+ else
69
+ print_bad "Invalid key: #{keys}"
70
+ end
71
+
72
+ else
73
+ print_error "Unknown key type"
74
+ raise
75
+ end
76
+ end
77
+
78
+
79
+ def handle_key
80
+ if File.file?(@list)
81
+ read_wordlist(@list)
82
+ else
83
+ @list
84
+ end
85
+ end
86
+
87
+ def read_wordlist(file)
88
+ if File.file?(file)
89
+ print_status "Found '#{file}' file."
90
+ File.readlines(file, chomp: true)
91
+ .lazy
92
+ .map(&:strip)
93
+ .reject(&:empty?)
94
+ .reject(&:nil?)
95
+ else
96
+ print_bad "File not found. #{file}"
97
+ exit!
98
+ end
99
+ end
100
+
101
+ end
102
+ end
103
+