timing_attack 0.4.1 → 0.5.0
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.
- checksums.yaml +4 -4
- data/README.md +1 -0
- data/bin/console +2 -9
- data/exe/timing_attack +88 -58
- data/lib/timing_attack/attacker.rb +33 -0
- data/lib/timing_attack/brute_forcer.rb +1 -2
- data/lib/timing_attack/enumerator.rb +1 -3
- data/lib/timing_attack/test_case.rb +24 -8
- data/lib/timing_attack/version.rb +1 -1
- data/lib/timing_attack.rb +1 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5287eec2e908eee5eb488df3f54f34331db4657e
|
4
|
+
data.tar.gz: 411d19b42241bd668205812445bae18139bb8292
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 494cc10f5f13d3fc1057c8b697cfa5ab3a66f1982f08d8178105729ec0c3feb672510408a53c426ad719197d1cbc2d81f79b5420cda67ae2bdb54818540d6470
|
7
|
+
data.tar.gz: d89c6979cb2cb61cd73bafaec6d5d5e0304a87e65e09ceec332dd0623d49e2384d22ba80fa81f6ff5912bd26f0abc0e9c7caad727ad6a70cf7a8dfd7940b0964
|
data/README.md
CHANGED
data/bin/console
CHANGED
@@ -3,12 +3,5 @@
|
|
3
3
|
require "bundler/setup"
|
4
4
|
require "timing_attack"
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
-
# require "pry"
|
11
|
-
# Pry.start
|
12
|
-
|
13
|
-
require "irb"
|
14
|
-
IRB.start
|
6
|
+
require "pry"
|
7
|
+
Pry.start
|
data/exe/timing_attack
CHANGED
@@ -1,68 +1,98 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
require 'timing_attack'
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
opts.on("-u URL", "--url URL", "URL of endpoint to profile") { |str| options[:url] = str }
|
9
|
-
opts.on("-n NUM", "--number NUM", "Requests per input (default: 50)") do |num|
|
10
|
-
options[:iterations] = num.to_i
|
4
|
+
class TimingAttackCli
|
5
|
+
attr_reader :options
|
6
|
+
def initialize
|
7
|
+
@options = {}
|
11
8
|
end
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
end
|
18
|
-
opts.on("-p", "--post", "Use POST, not GET") { |bool| options[:method] = bool ? :post : :get }
|
19
|
-
opts.on("-q", "--quiet", "Quiet mode (don't display progress bars)") { |bool| options[:verbose] = !bool }
|
20
|
-
opts.on("--brute_force", "Brute force mode") { |bool| options[:brute_force] = bool }
|
21
|
-
opts.on("--parameters STR", "JSON hash of parameters. 'INPUT' will be replaced with the attack string") do |str|
|
22
|
-
options[:params] = JSON.parse(str)
|
9
|
+
|
10
|
+
def run
|
11
|
+
parse_options
|
12
|
+
sanitize_options
|
13
|
+
execute_attack
|
23
14
|
end
|
24
|
-
|
25
|
-
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def opt_parser
|
19
|
+
@opt_parser ||= OptionParser.new do |opts|
|
20
|
+
opts.program_name = File.basename(__FILE__)
|
21
|
+
opts.banner = "#{opts.program_name} [options] -u <target> <inputs>"
|
22
|
+
opts.on("-u URL", "--url URL", "URL of endpoint to profile") { |str| options[:url] = str }
|
23
|
+
opts.on("-n NUM", "--number NUM", "Requests per input (default: 50)") do |num|
|
24
|
+
options[:iterations] = num.to_i
|
25
|
+
end
|
26
|
+
opts.on("-c NUM", "--concurrency NUM", "Number of concurrent requests (default: 15)") do |num|
|
27
|
+
options[:concurrency] = num.to_i
|
28
|
+
end
|
29
|
+
opts.on("-t NUM", "--threshold NUM", "Minimum threshold, in seconds, for meaningfulness (default: 0.025)") do |num|
|
30
|
+
options[:threshold] = num.to_f
|
31
|
+
end
|
32
|
+
opts.on("-p", "--post", "Use POST, not GET") { |bool| options[:method] = bool ? :post : :get }
|
33
|
+
opts.on("-q", "--quiet", "Quiet mode (don't display progress bars)") { |bool| options[:verbose] = !bool }
|
34
|
+
opts.on("-b", "--brute-force", "Brute force mode") { |bool| options[:brute_force] = bool }
|
35
|
+
opts.on("--parameters STR", "JSON hash of parameters. 'INPUT' will be replaced with the attack string") do |str|
|
36
|
+
options[:params] = JSON.parse(str)
|
37
|
+
end
|
38
|
+
opts.on("--body STR", "JSON of body paramets to be sent to Typhoeus. 'INPUT' will be replaced with the attack string") do |str|
|
39
|
+
options[:body] = JSON.parse(str)
|
40
|
+
end
|
41
|
+
opts.on("--http-username STR", "HTTP basic authentication username. 'INPUT' will be replaced with the attack string") do |str|
|
42
|
+
options[:basic_auth_username] = str
|
43
|
+
end
|
44
|
+
opts.on("--http-password STR", "HTTP basic authentication password. 'INPUT' will be replaced with the attack string") do |str|
|
45
|
+
options[:basic_auth_password] = str
|
46
|
+
end
|
47
|
+
opts.on("--percentile NUM", "Use NUMth percentile for calculations (default: 3)") { |num| options[:percentile] = num.to_i }
|
48
|
+
opts.on("--mean", "Use mean for calculations") { |bool| options[:mean] = bool }
|
49
|
+
opts.on("--median", "Use median for calculations") { |bool| options[:median] = bool }
|
50
|
+
opts.on_tail("-v", "--version", "Print version information") do
|
51
|
+
gem = Gem::Specification.find_by_name('timing_attack')
|
52
|
+
puts "#{gem.name} #{gem.version}"
|
53
|
+
exit
|
54
|
+
end
|
55
|
+
opts.on_tail("-h", "--help", "Display this screen") { puts opts ; exit }
|
56
|
+
end
|
26
57
|
end
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
58
|
+
|
59
|
+
def parse_options
|
60
|
+
begin
|
61
|
+
opt_parser.parse!
|
62
|
+
rescue OptionParser::InvalidOption => e
|
63
|
+
STDERR.puts e.message
|
64
|
+
puts opt_parser
|
65
|
+
exit
|
66
|
+
end
|
34
67
|
end
|
35
|
-
opts.on_tail("-h", "--help", "Display this screen") { puts opts ; exit }
|
36
|
-
end
|
37
68
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
options
|
46
|
-
|
47
|
-
|
48
|
-
elsif options[:median]
|
49
|
-
options[:percentile] = 50
|
50
|
-
elsif options[:mean]
|
51
|
-
options.delete(:percentile)
|
52
|
-
end
|
69
|
+
def sanitize_options
|
70
|
+
options[:verbose] = true if options[:verbose].nil?
|
71
|
+
if options[:percentile]
|
72
|
+
options.delete(:mean)
|
73
|
+
elsif options[:median]
|
74
|
+
options[:percentile] = 50
|
75
|
+
elsif options[:mean]
|
76
|
+
options.delete(:percentile)
|
77
|
+
end
|
78
|
+
end
|
53
79
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
80
|
+
def execute_attack
|
81
|
+
begin
|
82
|
+
atk = if options.delete(:brute_force)
|
83
|
+
TimingAttack::BruteForcer.new(options: options)
|
84
|
+
else
|
85
|
+
TimingAttack::Enumerator.new(inputs: ARGV, options: options)
|
86
|
+
end
|
87
|
+
atk.run!
|
88
|
+
rescue ArgumentError => e
|
89
|
+
STDERR.puts e.message
|
90
|
+
puts opt_parser
|
91
|
+
exit
|
92
|
+
rescue Interrupt
|
93
|
+
puts "\nCaught interrupt, exiting"
|
94
|
+
exit
|
95
|
+
end
|
96
|
+
end
|
68
97
|
end
|
98
|
+
TimingAttackCli.new.run
|
@@ -1,5 +1,15 @@
|
|
1
1
|
module TimingAttack
|
2
2
|
module Attacker
|
3
|
+
def initialize(options: {}, inputs: [])
|
4
|
+
@options = default_options.merge(options)
|
5
|
+
raise ArgumentError.new("Must provide url") if url.nil?
|
6
|
+
unless specified_input_option?
|
7
|
+
msg = "'#{INPUT_FLAG}' not found in url, parameters, body, or HTTP authentication options"
|
8
|
+
raise ArgumentError.new(msg)
|
9
|
+
end
|
10
|
+
raise ArgumentError.new("Iterations can't be < 3") if iterations < 3
|
11
|
+
end
|
12
|
+
|
3
13
|
def run!
|
4
14
|
if verbose?
|
5
15
|
puts "Target: #{url}"
|
@@ -29,7 +39,30 @@ module TimingAttack
|
|
29
39
|
concurrency: 15,
|
30
40
|
params: {},
|
31
41
|
body: {},
|
42
|
+
basic_auth_username: "",
|
43
|
+
basic_auth_password: ""
|
32
44
|
}.freeze
|
33
45
|
end
|
46
|
+
|
47
|
+
def option_contains_input?(obj)
|
48
|
+
case obj
|
49
|
+
when String
|
50
|
+
obj.include?(INPUT_FLAG)
|
51
|
+
when Symbol
|
52
|
+
option_contains_input?(obj.to_s)
|
53
|
+
when Array
|
54
|
+
obj.any? {|el| option_contains_input?(el) }
|
55
|
+
when Hash
|
56
|
+
option_contains_input?(obj.keys) || option_contains_input?(obj.values)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def input_options
|
61
|
+
@input_options ||= %i(basic_auth_password basic_auth_username body params url)
|
62
|
+
end
|
63
|
+
|
64
|
+
def specified_input_option?
|
65
|
+
input_options.any? { |opt| option_contains_input?(options[opt]) }
|
66
|
+
end
|
34
67
|
end
|
35
68
|
end
|
@@ -4,10 +4,8 @@ module TimingAttack
|
|
4
4
|
|
5
5
|
def initialize(inputs: [], options: {})
|
6
6
|
@inputs = inputs
|
7
|
-
@options = default_options.merge(options)
|
8
|
-
raise ArgumentError.new("url is a required argument") unless options.has_key? :url
|
9
7
|
raise ArgumentError.new("Need at least 2 inputs") if inputs.count < 2
|
10
|
-
|
8
|
+
super(options: options)
|
11
9
|
@attacks = inputs.map { |input| TestCase.new(input: input, options: @options) }
|
12
10
|
end
|
13
11
|
|
@@ -1,7 +1,6 @@
|
|
1
1
|
require 'uri'
|
2
2
|
module TimingAttack
|
3
3
|
class TestCase
|
4
|
-
INPUT_FLAG = "INPUT"
|
5
4
|
|
6
5
|
attr_reader :input
|
7
6
|
def initialize(input: , options: {})
|
@@ -16,20 +15,36 @@ module TimingAttack
|
|
16
15
|
)
|
17
16
|
@params = params_from(options.fetch :params, {})
|
18
17
|
@body = params_from(options.fetch :body, {})
|
18
|
+
@basic_auth_username = params_from(
|
19
|
+
options.fetch(:basic_auth_username, "")
|
20
|
+
)
|
21
|
+
@basic_auth_password = params_from(
|
22
|
+
options.fetch(:basic_auth_password, "")
|
23
|
+
)
|
19
24
|
end
|
20
25
|
|
21
26
|
def generate_hydra_request!
|
22
|
-
req = Typhoeus::Request.new(
|
23
|
-
url,
|
24
|
-
method: options.fetch(:method),
|
25
|
-
followlocation: true,
|
26
|
-
params: params,
|
27
|
-
body: body
|
28
|
-
)
|
27
|
+
req = Typhoeus::Request.new(url, **typhoeus_opts)
|
29
28
|
@hydra_requests.push req
|
30
29
|
req
|
31
30
|
end
|
32
31
|
|
32
|
+
def typhoeus_opts
|
33
|
+
{
|
34
|
+
method: options.fetch(:method),
|
35
|
+
followlocation: true,
|
36
|
+
}.tap do |h|
|
37
|
+
h[:params] = params unless params.empty?
|
38
|
+
h[:body] = body unless body.empty?
|
39
|
+
h[:userpwd] = typhoeus_basic_auth unless typhoeus_basic_auth.empty?
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def typhoeus_basic_auth
|
44
|
+
return "" if basic_auth_username.empty? && basic_auth_password.empty?
|
45
|
+
"#{basic_auth_username}:#{basic_auth_password}"
|
46
|
+
end
|
47
|
+
|
33
48
|
def process!
|
34
49
|
@hydra_requests.each do |request|
|
35
50
|
response = request.response
|
@@ -70,5 +85,6 @@ module TimingAttack
|
|
70
85
|
end
|
71
86
|
|
72
87
|
attr_reader :times, :options, :percentiles, :url, :params, :body
|
88
|
+
attr_reader :basic_auth_username, :basic_auth_password
|
73
89
|
end
|
74
90
|
end
|
data/lib/timing_attack.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: timing_attack
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Forrest Fleming
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-02-
|
11
|
+
date: 2017-02-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: ruby-progressbar
|