timing_attack 0.1.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 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: []