Class: Yawast::Scanner::Plugins::Applications::Generic::PasswordReset

Inherits:
Object
  • Object
show all
Defined in:
lib/scanner/plugins/applications/generic/password_reset.rb

Class Method Summary collapse

Class Method Details

.check_resp_user_enumObject



25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/scanner/plugins/applications/generic/password_reset.rb', line 25

def self.check_resp_user_enum
  begin
    # checks for user enum via differences in response
    # run each test 5 times to collect timing info
    good_user_res = fill_form_get_body @reset_page, @valid_user, true, true
    fill_form_get_body @reset_page, @valid_user, true, false
    fill_form_get_body @reset_page, @valid_user, true, false
    fill_form_get_body @reset_page, @valid_user, true, false
    fill_form_get_body @reset_page, @valid_user, true, false

    bad_user_res = fill_form_get_body @reset_page, SecureRandom.hex + '@invalid.example.com', false, true
    fill_form_get_body @reset_page, SecureRandom.hex + '@invalid.example.com', false, false
    fill_form_get_body @reset_page, SecureRandom.hex + '@invalid.example.com', false, false
    fill_form_get_body @reset_page, SecureRandom.hex + '@invalid.example.com', false, false
    fill_form_get_body @reset_page, SecureRandom.hex + '@invalid.example.com', false, false

    puts
    # check for difference in response
    if good_user_res != bad_user_res
      Yawast::Shared::Output.log_hash 'vulnerabilities',
                                      'password_reset_resp_user_enum',
                                      {vulnerable: true, url: @reset_page}

      Yawast::Utilities.puts_raw
      Yawast::Utilities.puts_vuln 'Password Reset: Possible User Enumeration - Difference In Response (see below for details)'
      Yawast::Utilities.puts_raw
      Yawast::Utilities.puts_raw Yawast::Utilities.diff_text(good_user_res, bad_user_res)
      Yawast::Utilities.puts_raw
      Yawast::Utilities.puts_raw
    else
      Yawast::Shared::Output.log_hash 'vulnerabilities',
                                      'password_reset_resp_user_enum',
                                      {vulnerable: false, url: @reset_page}
    end

    # check for timing issues
    valid_average = (@timing[true].inject(0, :+) / 5)
    invalid_average = (@timing[false].inject(0, :+) / 5)
    timing_diff = valid_average - invalid_average
    if timing_diff.abs > 10
      # in this case, we have a difference in the averages of greater than 10ms.
      # this is an arbitrary number, but 10ms is likely good enough
      Yawast::Utilities.puts_vuln 'Password Reset: Possible User Enumeration - Response Timing (see below for details)'
      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"
      Yawast::Utilities.puts_raw "\tValid Users     Invalid Users"
      Yawast::Utilities.puts_raw "\t-----------------------------"
      (0..4).each do |i|
        Yawast::Utilities.puts_raw "\t#{format('%.2f', @timing[true][i].round(2)).rjust(11)}"\
                                    "     #{format('%.2f', @timing[false][i].round(2)).rjust(13)}"
      end
      puts

      Yawast::Shared::Output.log_hash 'vulnerabilities',
                                      'password_reset_time_user_enum',
                                      {vulnerable: true, difference: timing_diff,
                                       valid_1: @timing[true][0], valid_2: @timing[true][1], valid_3: @timing[true][2],
                                       valid_4: @timing[true][3], valid_5: @timing[true][4],
                                       invalid_1: @timing[false][0], invalid_2: @timing[false][1], invalid_3: @timing[false][2],
                                       invalid_4: @timing[false][3], invalid_5: @timing[false][4]}
    else
      Yawast::Shared::Output.log_hash 'vulnerabilities',
                                      'password_reset_time_user_enum',
                                      {vulnerable: false, difference: timing_diff,
                                       valid_1: @timing[true][0], valid_2: @timing[true][1], valid_3: @timing[true][2],
                                       valid_4: @timing[true][3], valid_5: @timing[true][4],
                                       invalid_1: @timing[false][0], invalid_2: @timing[false][1], invalid_3: @timing[false][2],
                                       invalid_4: @timing[false][3], invalid_5: @timing[false][4]}
    end
  rescue ArgumentError => e
    Yawast::Utilities.puts_error "Unable to find a matching element to perform the User Enumeration via Password Reset Response test (#{e.message})"
  rescue => e # rubocop:disable Style/RescueStandardError
    Yawast::Utilities.puts_error "Failed to execute Password Reset Page User Enumeration: Error: #{e.message}"
  end
end

.fill_form_get_body(uri, user, valid, log_output) ⇒ Object



100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/scanner/plugins/applications/generic/password_reset.rb', line 100

def self.fill_form_get_body(uri, user, valid, log_output)
  options = Selenium::WebDriver::Chrome::Options.new({args: ['headless', 'incognito', 'disable-dev-shm-usage', 'no-sandbox']})

  # if we have a proxy set, use that
  if !Yawast.options.proxy.nil?
    proxy = Selenium::WebDriver::Proxy.new({http: "http://#{Yawast.options.proxy}", ssl: "http://#{Yawast.options.proxy}"})
    caps = Selenium::WebDriver::Remote::Capabilities.chrome({acceptInsecureCerts: true, proxy: proxy})
  else
    caps = Selenium::WebDriver::Remote::Capabilities.chrome({acceptInsecureCerts: true})
  end

  driver = Selenium::WebDriver.for(:chrome, {options: options, desired_capabilities: caps})
  driver.get uri

  # find the page form element - this is going to be a best effort thing, and may not always be right
  element = find_user_field driver

  # the element may not actually be visible yet (heavy JS pages)
  # so, we'll go into a loop for a few seconds to see if it'll show up
  counter = 0
  unless element.displayed?
    until element.displayed?
      sleep 0.5
      counter += 1

      break if counter > 20
    end
  end

  element.send_keys user

  beginning_time = Time.now
  element.submit
  end_time = Time.now
  @timing[valid].push((end_time - beginning_time) * 1000)

  res = driver.page_source
  img = driver.screenshot_as(:base64)

  valid_text = 'valid'
  valid_text = 'invalid' unless valid

  if log_output
    # log response
    Yawast::Shared::Output.log_hash 'applications',
                                    'password_reset_form',
                                    "pwd_reset_resp_#{valid_text}",
                                    {body: res, img: img, user: user}
  end

  driver.close

  res
end

.find_element(driver, name) ⇒ Object



184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/scanner/plugins/applications/generic/password_reset.rb', line 184

def self.find_element(driver, name)
  ret = nil

  # first, check by name
  begin
    ret =  driver.find_element({name: name})
  rescue # rubocop:disable Style/RescueStandardError, Lint/HandleExceptions
    # do nothing
  end

  # next, maybe it's id instead of name
  begin
    ret =  driver.find_element({id: name})
  rescue # rubocop:disable Style/RescueStandardError, Lint/HandleExceptions
    # do nothing
  end

  ret
end

.find_user_field(driver) ⇒ Object

Raises:

  • (ArgumentError)


155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
# File 'lib/scanner/plugins/applications/generic/password_reset.rb', line 155

def self.find_user_field(driver)
  # find the page form element - this is going to be a best effort thing, and may not always be right
  element = find_element driver, 'user_login'
  return element unless element.nil?

  element = find_element driver, 'email'
  return element unless element.nil?

  element = find_element driver, 'email_address'
  return element unless element.nil?

  element = find_element driver, 'forgetPasswordEmailOrUsername'
  return element unless element.nil?

  element = find_element driver, 'username'
  return element unless element.nil?

  # if we got here, it means that we don't have an element we know about, so we have to prompt
  if @element_name.nil?
    Yawast::Utilities.puts_raw 'Unable to find a known element to enter the user name. Please identify the proper element.'
    Yawast::Utilities.puts_raw 'If this element name seems to be common, please request that it be added: https://github.com/adamcaudill/yawast/issues'
    @element_name = Yawast::Utilities.prompt 'What is the user/email entry element name?'
  end
  element = find_element driver, @element_name
  return element unless element.nil?

  raise ArgumentError, 'No matching element found.'
end

.setupObject



12
13
14
15
16
17
18
19
20
21
22
23
# File 'lib/scanner/plugins/applications/generic/password_reset.rb', line 12

def self.setup
  @reset_page = Yawast.options.pass_reset_page

  @valid_user = if Yawast.options.user.nil?
                  Yawast::Utilities.prompt 'What is a valid user?'
                else
                  Yawast.options.user
                end

  @timing = {true => [], false => []}
  @element_name = nil
end