jwtear 0.2.0 → 1.0.0.pre

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+