Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/lib/metasploit/framework/login_scanner/base.rb
19813 views
1
require 'metasploit/framework/login_scanner'
2
3
module Metasploit
4
module Framework
5
module LoginScanner
6
7
# This module provides the base behaviour for all of
8
# the LoginScanner classes. All of the LoginScanners
9
# should include this module to establish base behaviour
10
module Base
11
extend ActiveSupport::Concern
12
include ActiveModel::Validations
13
14
included do
15
# @!attribute framework
16
# @return [Object] The framework instance object
17
attr_accessor :framework
18
# @!attribute framework_module
19
# @return [Object] The framework module caller, if available
20
attr_accessor :framework_module
21
# @!attribute connection_timeout
22
# @return [Integer] The timeout in seconds for a single SSH connection
23
attr_accessor :connection_timeout
24
# @!attribute cred_details
25
# @return [CredentialCollection] Collection of Credential objects
26
attr_accessor :cred_details
27
# @!attribute host
28
# @return [String] The IP address or hostname to connect to
29
attr_accessor :host
30
# @!attribute port
31
# @return [Integer] The port to connect to
32
attr_accessor :port
33
# @!attribute host
34
# @return [String] The local host for outgoing connections
35
attr_accessor :local_host
36
# @!attribute port
37
# @return [Integer] The local port for outgoing connections
38
attr_accessor :local_port
39
# @!attribute proxies
40
# @return [String] The proxy directive to use for the socket
41
attr_accessor :proxies
42
# @!attribute stop_on_success
43
# @return [Boolean] Whether the scanner should stop when it has found one working Credential
44
attr_accessor :stop_on_success
45
# @!attribute bruteforce_speed
46
# @return [Integer] The desired speed, with 5 being 'fast' and 0 being 'slow.'
47
attr_accessor :bruteforce_speed
48
# @!attribute sslkeylogfile
49
# @return [String] The SSL key log file path
50
attr_accessor :sslkeylogfile
51
52
validates :connection_timeout,
53
presence: true,
54
numericality: {
55
only_integer: true,
56
greater_than_or_equal_to: 1
57
}
58
59
validates :cred_details, presence: true
60
61
validates :host, presence: true
62
63
validates :port,
64
presence: true,
65
numericality: {
66
only_integer: true,
67
greater_than_or_equal_to: 1,
68
less_than_or_equal_to: 65535
69
}
70
71
validates :stop_on_success,
72
inclusion: { in: [true, false] }
73
74
validates :bruteforce_speed,
75
numericality: {
76
allow_nil: true,
77
only_integer: true,
78
greater_than_or_equal_to: 0,
79
less_than_or_equal_to: 5
80
}
81
82
validate :host_address_must_be_valid
83
84
validate :validate_cred_details
85
86
# @param attributes [Hash{Symbol => String,nil}]
87
def initialize(attributes={})
88
attributes.each do |attribute, value|
89
public_send("#{attribute}=", value)
90
end
91
set_sane_defaults
92
end
93
94
# Attempt a single login against the service with the given
95
# {Credential credential}.
96
#
97
# @param credential [Credential] The credential object to attempt to
98
# login with
99
# @return [Result] A Result object indicating success or failure
100
# @abstract Protocol-specific scanners must implement this for their
101
# respective protocols
102
def attempt_login(credential)
103
raise NotImplementedError
104
end
105
106
# @note Override this to detect that the service is up, is the right
107
# version, etc.
108
# @return [false] Indicates there were no errors
109
# @return [String] a human-readable error message describing why
110
# this scanner can't run
111
def check_setup
112
false
113
end
114
115
# @note Override this to set a timeout that makes more sense for
116
# your particular protocol. Telnet already usually takes a really
117
# long time, while MSSQL is often lickety-split quick. If
118
# overridden, the override should probably do something sensible
119
# with {#bruteforce_speed}
120
#
121
# @return [Integer] a number of seconds to sleep between attempts
122
def sleep_time
123
case bruteforce_speed
124
when 0; 60 * 5
125
when 1; 15
126
when 2; 1
127
when 3; 0.5
128
when 4; 0.1
129
else; 0
130
end
131
end
132
133
# A threadsafe sleep method
134
#
135
# @param time [Integer] number of seconds (can be a Float), defaults
136
# to {#sleep_time}
137
#
138
# @return [void]
139
def sleep_between_attempts(time=self.sleep_time)
140
::IO.select(nil,nil,nil,time) unless sleep_time.zero?
141
end
142
143
def each_credential
144
cred_details.each do |raw_cred|
145
146
# This could be a Credential object, or a Credential Core, or an Attempt object
147
# so make sure that whatever it is, we end up with a Credential.
148
credential = raw_cred.to_credential
149
150
if credential.realm.present? && self.class::REALM_KEY.present?
151
# The class's realm_key will always be the right thing for the
152
# service it knows how to login to. Override the credential's
153
# realm_key if one exists for the class. This can happen for
154
# example when we have creds for DB2 and want to try them
155
# against Postgres.
156
credential.realm_key = self.class::REALM_KEY
157
yield credential
158
elsif credential.realm.blank? && self.class::REALM_KEY.present? && self.class::DEFAULT_REALM.present?
159
# XXX: This is messing up the display for mssql when not using
160
# Windows authentication, e.g.:
161
# [+] 10.0.0.53:1433 - LOGIN SUCCESSFUL: WORKSTATION\sa:msfadmin
162
# Realm gets ignored in that case, so it still functions, it
163
# just gives the user bogus info
164
credential.realm_key = self.class::REALM_KEY
165
credential.realm = self.class::DEFAULT_REALM
166
yield credential
167
elsif credential.realm.present? && self.class::REALM_KEY.blank?
168
second_cred = credential.dup
169
# This service has no realm key, so the realm will be
170
# meaningless. Strip it off.
171
credential.realm = nil
172
credential.realm_key = nil
173
yield credential
174
# Some services can take a domain in the username like this even though
175
# they do not explicitly take a domain as part of the protocol.
176
# e.g., telnet
177
second_cred.public = "#{second_cred.realm}\\#{second_cred.public}"
178
second_cred.realm = nil
179
second_cred.realm_key = nil
180
yield second_cred
181
else
182
yield credential
183
end
184
end
185
end
186
187
# Attempt to login with every {Credential credential} in
188
# {#cred_details}, by calling {#attempt_login} once for each.
189
#
190
# If a successful login is found for a user, no more attempts
191
# will be made for that user.
192
#
193
# @yieldparam result [Result] The {Result} object for each attempt
194
# @yieldreturn [void]
195
# @return [void]
196
def scan!
197
valid!
198
199
# Keep track of connection errors.
200
# If we encounter too many, we will stop.
201
consecutive_error_count = 0
202
total_error_count = 0
203
204
successful_users = Set.new
205
ignored_users = Set.new
206
first_attempt = true
207
208
each_credential do |credential|
209
# Skip users for whom we've have already found a password
210
if successful_users.include?(credential.public)
211
# For Pro bruteforce Reuse and Guess we need to note that we
212
# skipped an attempt.
213
if credential.parent.respond_to?(:skipped)
214
credential.parent.skipped = true
215
credential.parent.save!
216
end
217
next
218
end
219
220
# Users that went into the lock-out list
221
if ignored_users.include?(credential.public)
222
if credential.parent.respond_to?(:skipped)
223
credential.parent.skipped = true
224
end
225
next
226
end
227
228
if first_attempt
229
first_attempt = false
230
else
231
sleep_between_attempts
232
end
233
234
result = attempt_login(credential)
235
result.freeze
236
237
yield result if block_given?
238
239
if result.success?
240
consecutive_error_count = 0
241
successful_users << credential.public
242
break if stop_on_success
243
elsif result.status == Metasploit::Model::Login::Status::LOCKED_OUT
244
ignored_users << credential.public
245
elsif result.status == Metasploit::Model::Login::Status::INVALID_PUBLIC_PART
246
ignored_users << credential.public
247
elsif result.status == Metasploit::Model::Login::Status::DISABLED
248
ignored_users << credential.public
249
else
250
if result.status == Metasploit::Model::Login::Status::UNABLE_TO_CONNECT
251
consecutive_error_count += 1
252
total_error_count += 1
253
break if consecutive_error_count >= 3
254
break if total_error_count >= 10
255
end
256
end
257
rescue => e
258
if framework_module
259
prefix = framework_module.respond_to?(:peer) ? "#{framework_module.peer} - LOGIN FAILED:" : "LOGIN FAILED:"
260
framework_module.print_warning("#{prefix} #{credential.to_h} - Unhandled error - scan may not produce correct results: #{e.message} - #{e.backtrace}")
261
end
262
elog("Scan Error: #{e.message}", error: e)
263
consecutive_error_count += 1
264
total_error_count += 1
265
break if consecutive_error_count >= 3
266
break if total_error_count >= 10
267
end
268
nil
269
end
270
271
# Raise an exception if this scanner's attributes are not valid.
272
#
273
# @raise [Invalid] if the attributes are not valid on this scanner
274
# @return [void]
275
def valid!
276
unless valid?
277
raise Metasploit::Framework::LoginScanner::Invalid.new(self)
278
end
279
nil
280
end
281
282
283
private
284
285
# This method validates that the host address is both
286
# of a valid type and is resolveable.
287
# @return [void]
288
def host_address_must_be_valid
289
if host.kind_of? String
290
begin
291
resolved_host = ::Rex::Socket.getaddress(host, true)
292
if host =~ /^\d{1,3}(\.\d{1,3}){1,3}$/
293
unless host =~ Rex::Socket::MATCH_IPV4
294
errors.add(:host, "could not be resolved")
295
end
296
end
297
self.host = resolved_host
298
rescue
299
errors.add(:host, "could not be resolved")
300
end
301
else
302
errors.add(:host, "must be a string")
303
end
304
end
305
306
# This is a placeholder method. Each LoginScanner class
307
# will override this with any sane defaults specific to
308
# its own behaviour.
309
# @abstract
310
# @return [void]
311
def set_sane_defaults
312
self.connection_timeout = 30 if self.connection_timeout.nil?
313
end
314
315
# This method validates that the credentials supplied
316
# are all valid.
317
# @return [void]
318
def validate_cred_details
319
unless cred_details.respond_to? :each
320
errors.add(:cred_details, "must respond to :each")
321
end
322
323
if cred_details.empty?
324
errors.add(:cred_details, "can't be blank")
325
end
326
end
327
328
end
329
330
331
end
332
333
end
334
end
335
end
336
337