yawast 0.7.0.beta1 → 0.7.0.beta2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +12 -0
  3. data/CHANGELOG.md +5 -1
  4. data/Gemfile +2 -2
  5. data/README.md +8 -1
  6. data/Rakefile +1 -1
  7. data/bin/yawast +8 -0
  8. data/lib/commands/cms.rb +2 -0
  9. data/lib/commands/dns.rb +3 -3
  10. data/lib/commands/head.rb +2 -0
  11. data/lib/commands/scan.rb +2 -0
  12. data/lib/commands/ssl.rb +2 -0
  13. data/lib/commands/utils.rb +5 -3
  14. data/lib/scanner/core.rb +34 -26
  15. data/lib/scanner/generic.rb +33 -130
  16. data/lib/scanner/plugins/applications/cms/generic.rb +20 -0
  17. data/lib/scanner/plugins/applications/generic/password_reset.rb +180 -0
  18. data/lib/scanner/plugins/dns/caa.rb +30 -12
  19. data/lib/scanner/plugins/dns/generic.rb +38 -1
  20. data/lib/scanner/plugins/http/directory_search.rb +14 -12
  21. data/lib/scanner/plugins/http/file_presence.rb +21 -13
  22. data/lib/scanner/plugins/http/generic.rb +95 -0
  23. data/lib/scanner/plugins/servers/apache.rb +23 -23
  24. data/lib/scanner/plugins/servers/generic.rb +25 -0
  25. data/lib/scanner/plugins/servers/iis.rb +6 -6
  26. data/lib/scanner/plugins/servers/nginx.rb +3 -1
  27. data/lib/scanner/plugins/servers/python.rb +3 -1
  28. data/lib/scanner/plugins/spider/spider.rb +7 -7
  29. data/lib/scanner/plugins/ssl/ssl.rb +14 -14
  30. data/lib/scanner/plugins/ssl/ssl_labs/analyze.rb +14 -13
  31. data/lib/scanner/plugins/ssl/ssl_labs/info.rb +6 -4
  32. data/lib/scanner/plugins/ssl/sweet32.rb +68 -63
  33. data/lib/scanner/ssl.rb +33 -36
  34. data/lib/scanner/ssl_labs.rb +373 -110
  35. data/lib/scanner/vuln_scan.rb +27 -0
  36. data/lib/shared/http.rb +31 -27
  37. data/lib/shared/output.rb +7 -15
  38. data/lib/shared/uri.rb +14 -14
  39. data/lib/string_ext.rb +10 -4
  40. data/lib/uri_ext.rb +1 -1
  41. data/lib/util.rb +28 -0
  42. data/lib/version.rb +3 -1
  43. data/lib/yawast.rb +12 -2
  44. data/test/data/ssl_labs_analyze_data_cam_hmhreservations_com.json +1933 -0
  45. data/test/test_scan_cms.rb +2 -2
  46. data/test/test_ssl_labs_analyze.rb +15 -0
  47. data/yawast.gemspec +8 -5
  48. metadata +75 -28
  49. data/lib/scanner/cms.rb +0 -14
  50. data/lib/scanner/php.rb +0 -19
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yawast
4
+ module Scanner
5
+ module Plugins
6
+ module Applications
7
+ module CMS
8
+ class Generic
9
+ def self.get_generator(body)
10
+ regex = /<meta name="generator[^>]+content\s*=\s*['"]([^'"]+)['"][^>]*>/
11
+ match = body.match regex
12
+
13
+ Yawast::Utilities.puts_info "Meta Generator: #{match[1]}" if match
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'selenium-webdriver'
4
+ require 'securerandom'
5
+
6
+ module Yawast
7
+ module Scanner
8
+ module Plugins
9
+ module Applications
10
+ module Generic
11
+ class PasswordReset
12
+ def self.setup
13
+ @reset_page = if Yawast.options.pass_reset_page.nil?
14
+ Yawast::Utilities.prompt 'What is the application password reset page?'
15
+ else
16
+ Yawast.options.pass_reset_page
17
+ end
18
+
19
+ @valid_user = if Yawast.options.user.nil?
20
+ Yawast::Utilities.prompt 'What is a valid user?'
21
+ else
22
+ Yawast.options.user
23
+ end
24
+
25
+ @timing = {true => [], false => []}
26
+ end
27
+
28
+ def self.check_resp_user_enum
29
+ begin
30
+ # checks for user enum via differences in response
31
+ # run each test 5 times to collect timing info
32
+ good_user_res = fill_form_get_body @reset_page, @valid_user, true, true
33
+ fill_form_get_body @reset_page, @valid_user, true, false
34
+ fill_form_get_body @reset_page, @valid_user, true, false
35
+ fill_form_get_body @reset_page, @valid_user, true, false
36
+ fill_form_get_body @reset_page, @valid_user, true, false
37
+
38
+ bad_user_res = fill_form_get_body @reset_page, SecureRandom.hex + '@invalid.example.com', false, true
39
+ fill_form_get_body @reset_page, SecureRandom.hex + '@invalid.example.com', false, false
40
+ fill_form_get_body @reset_page, SecureRandom.hex + '@invalid.example.com', false, false
41
+ fill_form_get_body @reset_page, SecureRandom.hex + '@invalid.example.com', false, false
42
+ fill_form_get_body @reset_page, SecureRandom.hex + '@invalid.example.com', false, false
43
+
44
+ puts
45
+ # check for difference in response
46
+ if good_user_res != bad_user_res
47
+ Yawast::Shared::Output.log_hash 'vulnerabilities',
48
+ 'password_reset_resp_user_enum',
49
+ {vulnerable: true, url: @reset_page}
50
+
51
+ Yawast::Utilities.puts_raw
52
+ Yawast::Utilities.puts_vuln 'Password Reset: Possible User Enumeration - Difference In Response (see below for details)'
53
+ Yawast::Utilities.puts_raw
54
+ Yawast::Utilities.puts_raw Yawast::Utilities.diff_text(good_user_res, bad_user_res)
55
+ Yawast::Utilities.puts_raw
56
+ Yawast::Utilities.puts_raw
57
+ else
58
+ Yawast::Shared::Output.log_hash 'vulnerabilities',
59
+ 'password_reset_resp_user_enum',
60
+ {vulnerable: false, url: @reset_page}
61
+ end
62
+
63
+ # check for timing issues
64
+ valid_average = (@timing[true].inject(0, :+) / 5)
65
+ invalid_average = (@timing[false].inject(0, :+) / 5)
66
+ timing_diff = valid_average - invalid_average
67
+ if timing_diff.abs > 10
68
+ # in this case, we have a difference in the averages of greater than 10ms.
69
+ # this is an arbitrary number, but 10ms is likely good enough
70
+ Yawast::Utilities.puts_vuln 'Password Reset: Possible User Enumeration - Response Timing (see below for details)'
71
+ Yawast::Utilities.puts_raw "\tDifference in average: #{timing_diff.abs.round(2)}ms Valid user: #{valid_average.round(2)}ms Invalid user: #{invalid_average.round(2)}ms"
72
+ Yawast::Utilities.puts_raw "\tValid Users Invalid Users"
73
+ Yawast::Utilities.puts_raw "\t-----------------------------"
74
+ (0..4).each do |i|
75
+ Yawast::Utilities.puts_raw "\t#{format('%.2f', @timing[true][i].round(2)).rjust(11)}"\
76
+ " #{format('%.2f', @timing[false][i].round(2)).rjust(13)}"
77
+ end
78
+ puts
79
+
80
+ Yawast::Shared::Output.log_hash 'vulnerabilities',
81
+ 'password_reset_time_user_enum',
82
+ {vulnerable: true, difference: timing_diff,
83
+ valid_1: @timing[true][0], valid_2: @timing[true][1], valid_3: @timing[true][2],
84
+ valid_4: @timing[true][3], valid_5: @timing[true][4],
85
+ invalid_1: @timing[false][0], invalid_2: @timing[false][1], invalid_3: @timing[false][2],
86
+ invalid_4: @timing[false][3], invalid_5: @timing[false][4]}
87
+ else
88
+ Yawast::Shared::Output.log_hash 'vulnerabilities',
89
+ 'password_reset_time_user_enum',
90
+ {vulnerable: false, difference: timing_diff,
91
+ valid_1: @timing[true][0], valid_2: @timing[true][1], valid_3: @timing[true][2],
92
+ valid_4: @timing[true][3], valid_5: @timing[true][4],
93
+ invalid_1: @timing[false][0], invalid_2: @timing[false][1], invalid_3: @timing[false][2],
94
+ invalid_4: @timing[false][3], invalid_5: @timing[false][4]}
95
+ end
96
+ rescue ArgumentError => e
97
+ Yawast::Utilities.puts "Unable to find a matching element to perform the User Enumeration via Password Reset Response test (#{e.message})"
98
+ end
99
+ end
100
+
101
+ def self.fill_form_get_body(uri, user, valid, log_output)
102
+ options = Selenium::WebDriver::Chrome::Options.new({args: ['headless']})
103
+
104
+ # if we have a proxy set, use that
105
+ if !Yawast.options.proxy.nil?
106
+ proxy = Selenium::WebDriver::Proxy.new({http: "http://#{Yawast.options.proxy}", ssl: "http://#{Yawast.options.proxy}"})
107
+ caps = Selenium::WebDriver::Remote::Capabilities.chrome({acceptInsecureCerts: true, proxy: proxy})
108
+ else
109
+ caps = Selenium::WebDriver::Remote::Capabilities.chrome({acceptInsecureCerts: true})
110
+ end
111
+
112
+ driver = Selenium::WebDriver.for(:chrome, {options: options, desired_capabilities: caps})
113
+ driver.get uri
114
+
115
+ # find the page form element - this is going to be a best effort thing, and may not always be right
116
+ element = find_user_field driver
117
+
118
+ element.send_keys user
119
+
120
+ beginning_time = Time.now
121
+ element.submit
122
+ end_time = Time.now
123
+ @timing[valid].push((end_time - beginning_time) * 1000)
124
+
125
+ res = driver.page_source
126
+ img = driver.screenshot_as(:base64)
127
+
128
+ valid_text = 'valid'
129
+ valid_text = 'invalid' unless valid
130
+
131
+ if log_output
132
+ # log response
133
+ Yawast::Shared::Output.log_hash 'applications',
134
+ 'password_reset_form',
135
+ "pwd_reset_resp_#{valid_text}",
136
+ {body: res, img: img, user: user}
137
+ end
138
+
139
+ driver.close
140
+
141
+ res
142
+ end
143
+
144
+ def self.find_user_field(driver)
145
+ # find the page form element - this is going to be a best effort thing, and may not always be right
146
+ element = find_element driver, 'user_login'
147
+ return element unless element.nil?
148
+
149
+ element = find_element driver, 'email'
150
+ return element unless element.nil?
151
+
152
+ element = find_element driver, 'email_address'
153
+ return element unless element.nil?
154
+
155
+ element = find_element driver, 'forgetPasswordEmailOrUsername'
156
+ return element unless element.nil?
157
+
158
+ # if we got here, it means that we don't have an element we know about, so we have to prompt
159
+ Yawast::Utilities.puts_raw 'Unable to find a known element to enter the user name. Please identify the proper element.'
160
+ Yawast::Utilities.puts_raw 'If this element name seems to be common, please request that it be added: https://github.com/adamcaudill/yawast/issues'
161
+ element_name = Yawast::Utilities.prompt 'What is the user/email entry element name?'
162
+ element = find_element driver, element_name
163
+ return element unless element.nil?
164
+
165
+ raise ArgumentError, 'No matching element found.'
166
+ end
167
+
168
+ def self.find_element(driver, name)
169
+ begin
170
+ return driver.find_element({name: name})
171
+ rescue ArgumentError
172
+ return nil
173
+ end
174
+ end
175
+ end
176
+ end
177
+ end
178
+ end
179
+ end
180
+ end
@@ -1,39 +1,56 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'dnsruby'
2
- include Dnsruby
3
4
 
4
5
  module Yawast
5
6
  module Scanner
6
7
  module Plugins
7
8
  module DNS
8
9
  class CAA
10
+ include Dnsruby
11
+
9
12
  def self.caa_info(uri)
10
13
  # force DNS resolver to something that works
11
14
  # this is done to ensure that ISP resolvers don't get in the way
12
15
  # at some point, should probably do something else, but works for now
13
- @res = Resolver.new({:nameserver => ['8.8.8.8']})
16
+ @res = Resolver.new({nameserver: ['8.8.8.8']})
14
17
 
15
18
  # setup a list of domains already checked, so we can skip them
16
- @checked = Array.new
19
+ @checked = []
20
+
21
+ # setup a counter, so we can see if we actually got anything
22
+ @records = 0
17
23
 
18
24
  domain = uri.host.to_s
19
25
 
20
26
  chase_domain domain
27
+
28
+ if @records.zero?
29
+ Yawast::Shared::Output.log_hash 'vulnerabilities',
30
+ 'missing_caa_records',
31
+ {vulnerable: true}
32
+
33
+ puts
34
+ Yawast::Utilities.puts_vuln 'DNS CAA: No records found.'
35
+ else
36
+ Yawast::Shared::Output.log_hash 'vulnerabilities',
37
+ 'missing_caa_records',
38
+ {vulnerable: false, record_count: @records}
39
+ end
21
40
  end
22
41
 
23
42
  def self.chase_domain(domain)
24
- while domain != '' do
43
+ while domain != ''
25
44
  begin
26
45
  # check to see if we've already ran into this one
27
- if @checked.include? domain
28
- return
29
- end
46
+ return if @checked.include? domain
30
47
  @checked.push domain
31
48
 
32
49
  # first, see if this is a CNAME. we do this explicitly because
33
50
  # some resolvers flatten in an odd way that prevents just checking
34
51
  # for the CAA record directly
35
52
  cname = get_cname_record(domain)
36
- if cname != nil
53
+ if !cname.nil?
37
54
  Yawast::Utilities.puts_info "\t\tCAA (#{domain}): CNAME Found: -> #{cname}"
38
55
  Yawast::Shared::Output.log_value 'dns', 'caa', domain, "CNAME: #{cname}"
39
56
 
@@ -41,7 +58,7 @@ module Yawast
41
58
  else
42
59
  print_caa_record domain
43
60
  end
44
- rescue => e
61
+ rescue => e # rubocop:disable Style/RescueStandardError
45
62
  Yawast::Utilities.puts_error "\t\tCAA (#{domain}): #{e.message}"
46
63
  end
47
64
 
@@ -53,7 +70,7 @@ module Yawast
53
70
  def self.get_cname_record(domain)
54
71
  ans = @res.query(domain, 'CNAME')
55
72
 
56
- if ans.answer[0] != nil
73
+ if !ans.answer[0].nil?
57
74
  return ans.answer[0].rdata
58
75
  else
59
76
  return nil
@@ -63,13 +80,14 @@ module Yawast
63
80
  def self.print_caa_record(domain)
64
81
  ans = @res.query(domain, 'CAA')
65
82
 
66
- if ans.answer.count > 0
83
+ if ans.answer.count.positive?
67
84
  ans.answer.each do |rec|
68
85
  # check for RDATA
69
- if rec.rdata != nil
86
+ if !rec.rdata.nil?
70
87
  Yawast::Utilities.puts_info "\t\tCAA (#{domain}): #{rec.rdata}"
71
88
 
72
89
  Yawast::Shared::Output.log_append_value 'dns', 'caa', domain, rec.rdata
90
+ @records += 1
73
91
  else
74
92
  Yawast::Utilities.puts_error "\t\tCAA (#{domain}): Invalid Response: #{ans.answer}"
75
93
  end
@@ -1,7 +1,11 @@
1
+ require 'dnsruby'
2
+
1
3
  module Yawast
2
4
  module Scanner
3
5
  module Plugins
4
6
  module DNS
7
+ include Dnsruby
8
+
5
9
  class Generic
6
10
  def self.dns_info(uri, options)
7
11
  begin
@@ -177,11 +181,23 @@ module Yawast
177
181
  end
178
182
 
179
183
  def self.find_subdomains(root_domain, resv)
184
+ res = Resolver.new()
185
+
180
186
  File.open(File.dirname(__FILE__) + '/../../../resources/subdomain_list.txt', 'r') do |f|
181
187
  f.each_line do |line|
182
188
  host = line.strip + '.' + root_domain
183
189
 
184
190
  begin
191
+ ## get CNAME records
192
+ cname = res.query(host, 'CNAME')
193
+ if cname.answer[0] != nil
194
+ Yawast::Utilities.puts_info "\t\tCNAME: #{host}: #{cname.answer[0].rdata}"
195
+
196
+ Yawast::Shared::Output.log_value 'dns', 'subdomain', host, cname.answer[0].rdata
197
+ next
198
+ end
199
+
200
+ ## get A records
185
201
  a = resv.getresources(host, Resolv::DNS::Resource::IN::A)
186
202
 
187
203
  unless a.empty?
@@ -195,6 +211,21 @@ module Yawast
195
211
  Yawast::Shared::Output.log_value 'dns', 'subdomain', host, ip.address
196
212
  end
197
213
  end
214
+
215
+ ## get AAAA records
216
+ aaaa = resv.getresources(host, Resolv::DNS::Resource::IN::AAAA)
217
+
218
+ unless aaaa.empty?
219
+ aaaa.each do |ip|
220
+ if IPAddr.new(ip.address.to_s, Socket::AF_INET6).private?
221
+ Yawast::Utilities.puts_info "\t\tAAAA: #{host}: #{ip.address}"
222
+ else
223
+ Yawast::Utilities.puts_info "\t\tAAAA: #{host}: #{ip.address} (#{get_network_info(ip.address)})"
224
+ end
225
+
226
+ Yawast::Shared::Output.log_value 'dns', 'subdomain', host, ip.address
227
+ end
228
+ end
198
229
  rescue
199
230
  #if this fails, don't really care
200
231
  end
@@ -223,7 +254,13 @@ module Yawast
223
254
  return ret
224
255
  rescue => e
225
256
  @netinfo_failed = true
226
- return "Error: getting network information failed (#{e.message})"
257
+
258
+ if e.message.include? 'unexpected token'
259
+ # this means that the service returned something invalid, like HTML
260
+ return "Error: getting network information failed (the service returned an unexpected message)"
261
+ else
262
+ return "Error: getting network information failed (#{e.message})"
263
+ end
227
264
  end
228
265
  end
229
266
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'securerandom'
2
4
 
3
5
  module Yawast
@@ -6,7 +8,7 @@ module Yawast
6
8
  module Http
7
9
  class DirectorySearch
8
10
  def self.search(uri, recursive, list_redirects, search_list = nil)
9
- #first, we need to see if the site responds to 404 in a reasonable way
11
+ # first, we need to see if the site responds to 404 in a reasonable way
10
12
  unless Yawast::Shared::Http.check_not_found(uri, false)
11
13
  puts 'Site does not respond properly to non-existent directory requests; skipping some checks.'
12
14
 
@@ -22,7 +24,7 @@ module Yawast
22
24
  puts 'Searching for common directories...'
23
25
  end
24
26
 
25
- if search_list == nil
27
+ if search_list.nil?
26
28
  @search_list = []
27
29
 
28
30
  File.open(File.dirname(__FILE__) + '/../../../resources/common_dir.txt', 'r') do |f|
@@ -39,7 +41,7 @@ module Yawast
39
41
  @jobs = Queue.new
40
42
  @results = Queue.new
41
43
 
42
- #load the queue, starting at /
44
+ # load the queue, starting at /
43
45
  base = uri.copy
44
46
  base.path = '/'
45
47
  load_queue base
@@ -50,7 +52,7 @@ module Yawast
50
52
  while (check = @jobs.pop(true))
51
53
  process check
52
54
  end
53
- rescue ThreadError
55
+ rescue ThreadError # rubocop:disable Lint/HandleExceptions
54
56
  #do nothing
55
57
  end
56
58
  end
@@ -59,19 +61,19 @@ module Yawast
59
61
  results = Thread.new do
60
62
  begin
61
63
  while true
62
- if @results.length > 0
64
+ if @results.length.positive?
63
65
  out = @results.pop(true)
64
66
  Yawast::Utilities.puts_info out
65
67
  end
66
68
  end
67
- rescue ThreadError
68
- #do nothing
69
+ rescue ThreadError # rubocop:disable Lint/HandleExceptions
70
+ # do nothing
69
71
  end
70
72
  end
71
73
 
72
74
  workers.map(&:join)
73
75
  results.terminate
74
- rescue => e
76
+ rescue => e # rubocop:disable Style/RescueStandardError
75
77
  Yawast::Utilities.puts_error "Error searching for directories (#{e.message})"
76
78
  end
77
79
 
@@ -85,10 +87,10 @@ module Yawast
85
87
  begin
86
88
  check.path = check.path + "#{line}/"
87
89
 
88
- #add the job to the queue
90
+ # add the job to the queue
89
91
  @jobs.push check
90
- rescue
91
- #who cares
92
+ rescue # rubocop:disable Style/RescueStandardError, Lint/HandleExceptions
93
+ # who cares
92
94
  end
93
95
  end
94
96
  end
@@ -106,7 +108,7 @@ module Yawast
106
108
  @results.push "\tFound Redirect: '#{uri} -> '#{res['Location']}'"
107
109
  Yawast::Shared::Output.log_value 'http', 'http_dir_redirect', uri, res['Location']
108
110
  end
109
- rescue => e
111
+ rescue => e # rubocop:disable Style/RescueStandardError
110
112
  unless e.message.include?('end of file') || e.message.include?('getaddrinfo')
111
113
  Yawast::Utilities.puts_error "Error searching for directory '#{uri.path}' (#{e.message})"
112
114
  end