CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

CoCalc provides the best real-time collaborative environment for Jupyter Notebooks, LaTeX documents, and SageMath, scalable from individual users to large groups and classes!

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