yawast 0.7.0.beta1 → 0.7.0.beta2

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.
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