timing_attack 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d03f591c320a5983d1a95d594e0ac067f79f9019
4
+ data.tar.gz: 587b251ddbe7ee8698fb9f1f6309ce2c0934b915
5
+ SHA512:
6
+ metadata.gz: 599c51ebac9e5b3ca49fec6d3364682427de3d6c85eda144b981c7138393cb658b2a88ac11f5a1eecef7b768569c3eb212a151634a34012758af18267fa05a26
7
+ data.tar.gz: c61b9cf40bcb33a9beed350d1f39b79a7f93c67430100c9fb021ca6bd23a72defc7a8814b7e7e5e78db61b2bbefe74ea02d3a5fa3a8eaf40e54433b6ba4e39bd
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.gem
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.2.4
5
+ before_install: gem install bundler -v 1.12.4
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in timing_attack.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,89 @@
1
+ # timing_attack
2
+
3
+ Profile web applications, sorting inputs into two categories based on
4
+ discrepancies in the application's response time.
5
+
6
+ ## Installation
7
+
8
+ ```bash
9
+ % gem install timing_attack
10
+ ```
11
+
12
+ ## Usage
13
+
14
+ ```
15
+ timing_attack [options] -a <input> -b <input> -u <target> <inputs>
16
+ -u, --url URL URL of endpoint to profile
17
+ -a, --a-example A_EXAMPLE Known test case that belongs to Group A
18
+ -b, --b-example B_EXAMPLE Known test case that belongs to Group B
19
+ -n, --number NUM Requests per input
20
+ --a-name A_NAME Name of Group A
21
+ --b-name B_NAME Name of Group B
22
+ -p, --post Use POST, not GET
23
+ -q, --quiet Quiet mode (don't display progress bars)
24
+ -v, --version Print version information
25
+ -h, --help Display this screen
26
+ ```
27
+
28
+ **NB**: If the provided examples are invalid, discvery will fail. Always check
29
+ your results! If very similar inputs are being sorted differently, you may have
30
+ used bad training data.
31
+
32
+ ### An example
33
+ Consider that we we want to gather information from a Rails server running
34
+ locally at `http://localhost:3000`. Let's say that we know the following:
35
+ * `charles@poodles.com` exists in the database
36
+ * `invalid@fake.fake` does not exist in the database
37
+
38
+ And we want to know if `candidate@address.com` and `other@address.com` exist in
39
+ the database.
40
+
41
+ We execute (using `-q` to suppress the progress bar)
42
+ ```bash
43
+ % timing_attack -q -u http://localhost:3000/login \
44
+ -a charles@poodles.com -b invalid@fake.fake \
45
+ candidate@address.com other@address.com
46
+ ```
47
+ ```
48
+ Group A:
49
+ candidate@address.com ~0.1969s
50
+ Group B:
51
+ other@address.com ~0.1096s
52
+ ```
53
+ `candidate@address.com` is in the same group as `charles@poodles.com` (Group A),
54
+ while `other@address.com` is in Group B with `invalid@fake.fake`
55
+ Thus we know that `candidate@address.com` exists in the database, and that
56
+ `other@example.com` does not.
57
+
58
+ To make things a bit friendlier, we can rename groups with the `--a-name` and
59
+ `--b-name` options:
60
+ ```bash
61
+ % timing_attack -q -u http://localhost:3000/login \
62
+ -a charles@poodles.com -b invalid@fake.fake \
63
+ --a-name 'Valid logins' --b-name 'Invalid logins' \
64
+ candidate@address.com other@address.com
65
+ ```
66
+ ```
67
+ Valid logins:
68
+ candidate@address.com ~0.1988s
69
+ Invalid logins:
70
+ other@address.com ~0.1065s
71
+ ```
72
+
73
+ ## Contributing
74
+
75
+ Bug reports and pull requests are welcome [here](https://github.com/ffleming/timing_attack).
76
+
77
+ ## Disclaimer
78
+
79
+ TimingAttack is quick and dirty. It is meant to exploit *known* timing attacks based
80
+ upon two known values. TimingAttack is *not* for discovering the existence of
81
+ timing-based vulnerabilities.
82
+
83
+ Also, don't use TimingAttack against machines that aren't yours.
84
+
85
+ ## Todo
86
+ * Tests
87
+ * Better heuristic than naïve mean comparison
88
+ * Auto-discovering heuristic that doesn't require example test cases
89
+ * Customizable query parameters
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "timing_attack"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
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
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/exe/timing_attack ADDED
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env ruby
2
+ require 'timing_attack'
3
+ require 'optparse'
4
+
5
+ options = {}
6
+ opt_parser = OptionParser.new do |opts|
7
+ opts.program_name = File.basename(__FILE__)
8
+ opts.banner = "#{opts.program_name} [options] -a <input> -b <input> -u <target> <inputs>"
9
+ opts.on("-u URL", "--url URL", "URL of endpoint to profile") { |str| options[:url] = str }
10
+ opts.on("-a A_EXAMPLE", "--a-example A_EXAMPLE", "Known test case that belongs to Group A") { |str| options[:a_example] = str }
11
+ opts.on("-b B_EXAMPLE", "--b-example B_EXAMPLE", "Known test case that belongs to Group B") { |str| options[:b_example] = str }
12
+ opts.on("-n NUM", "--number NUM", "Requests per input") { |num| options[:iterations] = num.to_i }
13
+ opts.on("--a-name A_NAME", "Name of Group A") { |str| options[:a_name] = str }
14
+ opts.on("--b-name B_NAME", "Name of Group B") { |str| options[:b_name] = str }
15
+ opts.on("-p", "--post", "Use POST, not GET") { |bool| options[:method] = bool ? :post : :get }
16
+ opts.on("-q", "--quiet", "Quiet mode (don't display progress bars)") { |bool| options[:verbose] = !bool }
17
+ opts.on_tail("-v", "--version", "Print version information") do
18
+ gem = Gem::Specification.find_by_name('timing_attack')
19
+ puts "#{gem.name} #{gem.version}"
20
+ exit
21
+ end
22
+ opts.on_tail("-h", "--help", "Display this screen") { puts opts ; exit }
23
+ end
24
+
25
+ begin
26
+ opt_parser.parse!
27
+ rescue OptionParser::InvalidOption => e
28
+ STDERR.puts e.message
29
+ puts opt_parser
30
+ exit
31
+ end
32
+
33
+ begin
34
+ atk = TimingAttack::Attacker.new(inputs: ARGV, options: options)
35
+ atk.run!
36
+ puts atk
37
+ rescue ArgumentError => e
38
+ STDERR.puts e.message
39
+ puts opt_parser
40
+ exit
41
+ end
@@ -0,0 +1,130 @@
1
+ module TimingAttack
2
+ class Attacker
3
+ def initialize(inputs: [], options: {})
4
+ @options = DEFAULT_OPTIONS.merge(options)
5
+ unless @options.has_key? :width
6
+ @options[:width] = [a_name, b_name, *inputs].map(&:length).push(30).sort.last
7
+ end
8
+ %i(a_example b_example url).each do |arg|
9
+ raise ArgumentError.new("#{arg} is a required argument") unless options.has_key? arg
10
+ end
11
+ @attacks = inputs.map { |input| TestCase.new(input: input, options: @options) }
12
+ end
13
+
14
+ def run!
15
+ puts "Target: #{url}" if verbose?
16
+ warmup!
17
+ benchmark!
18
+ attack!
19
+ end
20
+
21
+ def to_s
22
+ ret = ""
23
+ if verbose?
24
+ ret << "Benchmark results\n"
25
+ ret << " #{a_name.ljust(width)}~#{sprintf('%.4f', a_benchmark.mean_time,)}s\n"
26
+ ret << " #{b_name.ljust(width)}~#{sprintf('%.4f', b_benchmark.mean_time)}s\n"
27
+ end
28
+ ret << attack_string
29
+ end
30
+
31
+ private
32
+
33
+ attr_reader :attacks, :options
34
+
35
+ def warmup!
36
+ @warmup_test ||= TestCase.new(input: a_example, options: options)
37
+ 2.times { @warmup_test.test! }
38
+ end
39
+
40
+ def benchmark!
41
+ iterations.times do
42
+ [a_benchmark, b_benchmark].each do |test_case|
43
+ test_case.test!
44
+ benchmark_bar.increment
45
+ end
46
+ end
47
+ end
48
+
49
+ def attack!
50
+ iterations.times do
51
+ attacks.each do |attack|
52
+ attack.test!
53
+ attack_bar.increment
54
+ end
55
+ end
56
+ attacks.each { |attack| attack.derive_group_from(a_test: a_benchmark, b_test: b_benchmark) }
57
+ end
58
+
59
+ def a_benchmark
60
+ @a_benchmark ||= TestCase.new(input: a_example, options: options)
61
+ end
62
+
63
+ def b_benchmark
64
+ @b_benchmark ||= TestCase.new(input: b_example, options: options)
65
+ end
66
+
67
+ def indent(str)
68
+ " #{str.ljust(width)}"
69
+ end
70
+
71
+ def a_attacks
72
+ attacks.select { |a| a.group_a? }
73
+ end
74
+
75
+ def b_attacks
76
+ attacks.select { |a| a.group_b? }
77
+ end
78
+
79
+ def attack_string
80
+ ret = ""
81
+ unless a_attacks.empty?
82
+ ret << "#{a_name}:\n"
83
+ ret << a_attacks.map {|a| indent(a.to_s)}.join("\n")
84
+ end
85
+ unless b_attacks.empty?
86
+ ret << "\n#{b_name}:\n"
87
+ ret << b_attacks.map {|a| indent(a.to_s)}.join("\n")
88
+ end
89
+ "#{ret}\n"
90
+ end
91
+
92
+ def attack_bar
93
+ return null_bar unless verbose?
94
+ @attack_bar ||= ProgressBar.create(title: " Attacking".ljust(15),
95
+ total: iterations * attacks.length,
96
+ format: bar_format
97
+ )
98
+ end
99
+
100
+ def benchmark_bar
101
+ return null_bar unless verbose?
102
+ @benchmark_bar ||= ProgressBar.create(title: " Benchmarking".ljust(15),
103
+ total: iterations * 2,
104
+ format: bar_format
105
+ )
106
+ end
107
+
108
+ def bar_format
109
+ @bar_format ||= "%t (%E) |%B|"
110
+ end
111
+
112
+ def null_bar
113
+ @null_bar_klass ||= Struct.new('NullProgressBar', :increment)
114
+ @null_bar ||= @null_bar_klass.new
115
+ end
116
+
117
+ %i(iterations url verbose a_name b_name a_example b_example width method).each do |sym|
118
+ define_method(sym) { options.fetch sym }
119
+ end
120
+ alias_method :verbose?, :verbose
121
+
122
+ DEFAULT_OPTIONS = {
123
+ verbose: true,
124
+ a_name: "Group A",
125
+ b_name: "Group B",
126
+ method: :get,
127
+ iterations: 5
128
+ }.freeze
129
+ end
130
+ end
@@ -0,0 +1,56 @@
1
+ module TimingAttack
2
+ class TestCase
3
+ attr_reader :input
4
+ def initialize(input: , options: {})
5
+ @input = input
6
+ @options = options
7
+ @times = []
8
+ end
9
+
10
+ def test!
11
+ httparty_opts = {
12
+ body: {
13
+ login: input,
14
+ password: "test" * 1000
15
+ },
16
+ timeout: 5
17
+ }
18
+ before = Time.now
19
+ HTTParty.send(options.fetch(:method), options.fetch(:url), httparty_opts)
20
+ diff = (Time.now - before)
21
+ times.push(diff)
22
+ end
23
+
24
+ def to_s
25
+ "#{input.ljust(options.fetch(:width))}~#{sprintf('%.4f', mean_time)}s"
26
+ end
27
+
28
+ def derive_group_from(a_test: , b_test: )
29
+ unless a_test.is_a?(TestCase) && b_test.is_a?(TestCase)
30
+ raise ArgumentError.new("a_test and b_test must be TestCases")
31
+ end
32
+ d_a = (mean_time - a_test.mean_time).abs
33
+ d_b = (mean_time - b_test.mean_time).abs
34
+ @group_a = (d_a < d_b)
35
+ end
36
+
37
+ def group_a
38
+ raise ArgumentError.new("Have not yet determined group membership") if @group_a.nil?
39
+ @group_a
40
+ end
41
+ alias_method :group_a?, :group_a
42
+
43
+ def group_b
44
+ !group_a
45
+ end
46
+ alias_method :group_b?, :group_b
47
+
48
+ def mean_time
49
+ times.reduce(:+) / times.size.to_f
50
+ end
51
+
52
+ private
53
+
54
+ attr_reader :times, :options
55
+ end
56
+ end
@@ -0,0 +1,3 @@
1
+ module TimingAttack
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,8 @@
1
+ require 'httparty'
2
+ require 'ruby-progressbar'
3
+ require "timing_attack/version"
4
+ require "timing_attack/test_case"
5
+ require "timing_attack/attacker"
6
+
7
+ module TimingAttack
8
+ end
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'timing_attack/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "timing_attack"
8
+ spec.version = TimingAttack::VERSION
9
+ spec.authors = ["Forrest Fleming"]
10
+ spec.email = ["ffleming@gmail.com"]
11
+
12
+ spec.summary = "Performtiming attacks against web applications"
13
+ spec.description = "Profile web applications by noting differences in response times based on input values"
14
+ spec.homepage = "https://www.github.com/ffleming/timing_attack"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
+ spec.bindir = "exe"
18
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
+ spec.require_paths = ["lib"]
20
+ spec.licenses = %q(MIT)
21
+
22
+ spec.add_runtime_dependency "ruby-progressbar", "~> 1.8"
23
+ spec.add_runtime_dependency "httparty", "~> 0.13.3"
24
+ spec.add_development_dependency "bundler", "~> 1.12"
25
+ spec.add_development_dependency "rake", "~> 10.0"
26
+ spec.add_development_dependency "rspec", "~> 3.0"
27
+ end
metadata ADDED
@@ -0,0 +1,130 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: timing_attack
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Forrest Fleming
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-07-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: ruby-progressbar
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.8'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.8'
27
+ - !ruby/object:Gem::Dependency
28
+ name: httparty
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.13.3
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.13.3
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.12'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.12'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '10.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '10.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.0'
83
+ description: Profile web applications by noting differences in response times based
84
+ on input values
85
+ email:
86
+ - ffleming@gmail.com
87
+ executables:
88
+ - timing_attack
89
+ extensions: []
90
+ extra_rdoc_files: []
91
+ files:
92
+ - ".gitignore"
93
+ - ".rspec"
94
+ - ".travis.yml"
95
+ - Gemfile
96
+ - README.md
97
+ - Rakefile
98
+ - bin/console
99
+ - bin/setup
100
+ - exe/timing_attack
101
+ - lib/timing_attack.rb
102
+ - lib/timing_attack/attacker.rb
103
+ - lib/timing_attack/test_case.rb
104
+ - lib/timing_attack/version.rb
105
+ - timing_attack.gemspec
106
+ homepage: https://www.github.com/ffleming/timing_attack
107
+ licenses:
108
+ - MIT
109
+ metadata: {}
110
+ post_install_message:
111
+ rdoc_options: []
112
+ require_paths:
113
+ - lib
114
+ required_ruby_version: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ required_rubygems_version: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ requirements: []
125
+ rubyforge_project:
126
+ rubygems_version: 2.4.8
127
+ signing_key:
128
+ specification_version: 4
129
+ summary: Performtiming attacks against web applications
130
+ test_files: []